Zend Form - Trabalhando com formulários no Zend Framework 2 e 3

Post publicado em 27/08/2018 09:00 Última atualização em 27/08/2018 09:53

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.

Requisitos

Prévio conhecimento de PHP, Composer e já ter lido o post sobre o início com Zend ou criado um projeto através do Skeleton Application. Detalhe: no momento da instalação opte pela instalação completa, com todos os helpers e componentes sugeridos.

O começo

Quando ouvir falar de Zend Form quero que você tenha em mente a utilização de todo o ecossistema e não somente da classe Form. De forma resumida, defini o meu conceito de Zend Form com uma classe do formulário, uma classe de filtros, renderização na view, tratativas no controller e testes de tudo isso. Veja abaixo uma possível estrutura de pastas para o cenário que acabei de descrever.   Então vamos estruturar nossa aplicação.

Form

Crie o seguinte arquivo: module/Application/Form/ExampleForm.php e nele a seguinte estrutura inicial.
<?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);

Estrutura comum

Todos os elementos que criaremos possuem algumas definições comuns, sendo elas o name, type, options e attributes.

Text

# 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'
    ]
]);

Textarea

# 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'
    ]
]);

Number

# 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'
    ]
]);

Select

# 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.

Checkbox

# 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'
    ]
]);

Radio

# 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.

Submit

# 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.

Concluindo

Pronto, nosso classe de form está configurada com os principais elementos utilizados no dia a dia. Ainda existem diversos outros que você pode utilizar como Button, Captcha, Color, Date, Email, Hidden e Range. Explore o componente Zend Form.  

View

Crie o arquivo module/Application/view/application/index/add.phtml com o conteúdo a seguir. Adicionei comentários nos pontos que precisavam de explicação.
<?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();

No controller

Como o form é recuperado na view, temos de o passar no ViewModel no controller correspondente. Vou levar em consideração que você já tem a rota apontando para o \Application\Controller\IndexController e na action add, tratarei somente o envio do form.
# Nos imports da classe
use Application\Form\ExampleForm;

public function addAction()
{
    $form = new ExampleForm();

    return new ViewModel([
        'form' => $form
    ]);
}

Resultado

Entrando em nossa aplicação, na rota que você definiu para o add (no meu caso /application/add), o resultado deve ser como este.

Form Filter

Até agora criamos a estrutura de nosso form, conhecendo alguns elementos e renderizando os mesmos na view. Pois bem, se um usuário de nosso site ou sistema digitar os dados e nós simplesmente confiar neles, estaremos correndo um grande risco. E é aí que entra o Form Filter, uma forma programática de aplicar diversos filtros e validações dos dados inputados. O Form Filter é uma classe bem parecida com a classe Form que criamos momentos atrás. Nela definimos cada um dos elementos, seus filtros e as validações que queremos fazer nos mesmos. A estrutura básica do Form Filter é como no arquivo abaixo. Crie o mesmo em module/Application/src/Form/ExampleFormFilter.php.
<?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.

Filters

Pegando o campo text previamente criado no form, podemos aplicar alguns filtros como StripTags e StringTrim. Estes filtros servem para limpar o conteúdo digitado pelo usuário afim de dificultar ataques xss. Além de muitos filtros que o próprio Zend já fornece através do componente zend-filter, você pode criar seus próprios filtros.

Validators

Servem para que possamos aplicar regras de validações para nossos input do form. Da mesma forma que os filtros, você pode criar suas próprias validações. Com exemplo eu mostro um validador que é bem comum utilizarmos e que requer algumas configurações distintas do que já apresentado no exemplo anterior: validação da confirmação de senha. Pensemos em dois campos em nosso form, um password e outro password_confirmation.
# 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.  

Tratando erros

Como bem sabemos, cada um dos validadores possui uma série de mensagens de erro, como por exemplo o Identical, recém apresentado. Abaixo segue as mensagens padrão do Identical. Mas podemos alterar a mensagem de erro caso as senhas não sejam as mesmas. O nosso elemento password_confirmation em ExampleFormFilter fica da seguinte maneira.
$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'
                ]
            ]
        ]
    ]
]);

No form

Apesar de termos definido corretamente nossos filtros e validadores, nosso form até o momento não tem conhecimento disso. Resta então especificar o input filter no nosso Application\Form\ExampleForm. Logo após o parent::__construct e a definição do nosso enctype, adicione as seguintes linhas:
$exampleFormFilter = new ExampleFormFilter();
$this->setInputFilter($exampleFormFilter->getInputFilter());

// E o restante dor form segue como já definido anteriormente
//$this->add([...

No controller

Até o momento apenas passamos o form para a view, mas não definimos como os erros serão tratados. Vamos fazer isso agora. O seu método addAction deve ficar como este:
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:

Na view

Agora que no controller já temos o correto tratamento para os erros, precisamos os renderizar na view. O processo é bem simples, verificar se na variável $this->errorMessages existe a chave condizente com o nome do campo, como visível na imagem acima. No entanto tem um detalhe, pode existir mais de um erro, com isso precisamos criar um loop dos erros na view.
<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.

Testes

Sou defensor extremo do processo correto do TDD, primeiro o teste, depois o código que resolve o problema. No entanto como este é um post extenso e para alguns o assunto de testes automatizados pode não interessar, o deixei para o final. Se você iniciou o projeto a partir do Zend Skeleton Application, e seu arquivo do phpunit ainda esteja com a extensão .dist, basta o renomear de phpunit.xml.dist para phpunit.xml. Não precisa de nenhuma configuração adicional para rodar os testes, basta através do composer rodar o seguinte comando:
composer test
Veja 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>

ExampleFormTest

Crie o arquivo module/Application/test/Form/ExampleFormTest.php e nele o seguinte conteúdo. Adicionei os comentários no próprio código para que fique mais simples de entender.
<?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());
    }
}

Rodando os testes

Ao rodar os testes exatamente da forma como você copiou este código, um erro estará presente. Qual é o erro? Veja na imagem abaixo.
Solução
Em nosso checkbox não definimos as opções do mesmo. O erro lançado é referente ao valor passado (on) não estar na pilha de valores disponíveis. Basta adicionar os possíveis valores que o teste passa.
$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!

ExampleFormFilterTest

Agora vamos criar nossos testes para a classe de filtros e validações. Este teste é mais simples, basta verificar se os campos existem.
<?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.  

Cobertura de testes

Caso você tenha o xdebug instalado, pode ainda configurar sua suite de testes para colher feedback e melhorar a cobertura dos seus testes. No exemplo abaixo, configuro o phpunit para gerar coverage em formato html.
<?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.

Conclusão

Espero que você tenha gostado deste conteúdo, fiz com muita dedicação. Ainda restaram muitos pontos a abordar, quem sabe crio novos posts com os mais específicos como upload de arquivos, captcha, dentre outros. Só não fiz aqui para que não fique muito extenso. Como você pode ver a utilização do Zendo Form pode num primeiro momento parecer um pouco assustadora, no entanto, como usuário assíduo deste robusto framework, eu garanto, com o dia a dia as coisas se tornam muito mais simples. O lado bom do Zend é que você faz as coisas uma vez, bem feitinho, e as reaproveita sempre. Hoje estou construindo uma aplicação base com o Zend Framework com as coisas mais comuns que desenvolvo em sistemas. O nome do projeto é zf-base e este é o link do github. Isso vem me agilizanfo muito, a cada novo projeto, já tenho uma base funcional, basta inicializar e focar na resolução do problema do projeto em que estou trabalhando no momento. Com este post, tenho a intenção de que você tenha aprendido como utilizar este poderoso componente do Zend e deixar suas aplicações ainda mais seguras. As validações JavaScript podem ser facilmente desabilitadas pelo usuário, mas as validações no Backend ele não tem poder para manipular. Quer todo o código que foi criado aqui? Então pega lá no github ;) Quer saber mais sobre testes automatizados e TDD? Veja isso.


Scroll down