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.
Então vamos estruturar nossa aplicação.
<?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.
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'
]
]
]
]
]);
$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());
}
}
Qual é o erro? Veja na imagem abaixo.

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