Trabalhando com formulários
Esta seção continua a melhorar o "Dizendo olá". Em vez de usar a URL, agora você vai pedir a mensagem ao usuário por meio de um formulário.
Neste tutorial, você vai aprender a:
- Criar um form model para representar os dados que o usuário informa em um formulário.
- Declarar regras para validar os dados informados.
- Montar um formulário HTML em uma view.
Instalando o pacote de formulários
Para instalar o pacote de formulários, rode o seguinte comando no diretório da sua aplicação:
composer require yiisoft/form-modelNo Docker, fica assim:
make composer require yiisoft/form-modelCriando um formulário
Os dados que serão pedidos ao usuário ficam representados por uma classe Form, como mostrado abaixo, salva no arquivo /src/Web/Echo/Form.php:
<?php
declare(strict_types=1);
namespace App\Web\Echo;
use Yiisoft\FormModel\FormModel;
use Yiisoft\Validator\Label;
use Yiisoft\Validator\Rule\Length;
final class Form extends FormModel
{
#[Label('The message to be echoed')]
#[Length(min: 2)]
public string $message = '';
}No exemplo acima, o Form tem uma única propriedade string, $message, cujo tamanho deve ser de pelo menos dois caracteres. Também há um label personalizado para a propriedade.
Nota: os atributos de validação são regras do Yii Validator. Para verificações comuns, procure primeiro uma regra existente. Se um campo também precisar de validação específica da aplicação, use
Callbackpara essa lógica de domínio. Com atributos PHP, use a opçãomethod, porque atributos PHP não podem conter closures. Por exemplo, valide a sintaxe do UUID comUuide só então adicione#[Callback(method: 'validateUuidV7', skipOnError: true)]quando o seu domínio exigir especificamente UUID versão 7.
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Callback;
use Yiisoft\Validator\Rule\Uuid;
#[Uuid(skipOnError: true)]
#[Callback(method: 'validateUuidV7', skipOnError: true)]
public string $id = '';
private function validateUuidV7(mixed $value): Result
{
return \App\Support\Uuid::isVersion7($value)
? new Result()
: (new Result())->addError('ID must be a UUID version 7.');
}O método de callback retorna um Result: retorne um resultado vazio quando o valor for válido ou adicione um erro quando não for. O corpo do método pode chamar qualquer código de validação que a sua aplicação já usa, como App\Support\Uuid::isValid($value, 'v7'). Veja os detalhes da regra Callback para exemplos completos e as assinaturas de método disponíveis.
Para uma lógica de validação reaproveitada em vários formulários, crie uma regra e um handler personalizados do Yii Validator em vez de copiar métodos de callback. Veja o guia de regras personalizadas para a estrutura de regra/handler.
Validação personalizada
Os atributos de validação são regras do Yii Validator. Para verificações comuns, procure primeiro uma regra existente. Por exemplo, para validar uma string UUID, use Uuid:
use Yiisoft\FormModel\FormModel;
use Yiisoft\Validator\Rule\Uuid;
final class Form extends FormModel
{
#[Uuid]
public string $id = '';
}Quando um campo precisa de uma validação específica da aplicação que nenhuma regra embutida cobre, use Callback. Isso é útil para verificações pontuais ou para delegar a um método do domínio ou de uma biblioteca. Com atributos PHP, use a opção method, porque atributos PHP não podem conter closures. Por exemplo, o formulário a seguir valida o dígito verificador de um número de cartão:
<?php
declare(strict_types=1);
namespace App\Web\Echo;
use Yiisoft\FormModel\FormModel;
use Yiisoft\Validator\Label;
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Callback;
use Yiisoft\Validator\Rule\Required;
use Yiisoft\Validator\ValidationContext;
final class Form extends FormModel
{
#[Label('Card number')]
#[Required]
#[Callback(method: 'validateCardNumberChecksum', skipOnError: true)]
public string $cardNumber = '';
private function validateCardNumberChecksum(mixed $value, Callback $rule, ValidationContext $context): Result
{
if (!is_string($value)) {
return (new Result())->addError('Card number must be a string.');
}
$digits = str_replace([' ', '-'], '', $value);
if ($digits === '' || !ctype_digit($digits)) {
return (new Result())->addError('Card number must contain digits only.');
}
$sum = 0;
$double = false;
foreach (array_reverse(str_split($digits)) as $digit) {
$number = (int) $digit;
if ($double) {
$number *= 2;
if ($number > 9) {
$number -= 9;
}
}
$sum += $number;
$double = !$double;
}
if ($sum % 10 !== 0) {
return (new Result())->addError('Card number checksum is invalid.');
}
return new Result();
}
}O método de callback retorna um Result: retorne um resultado vazio quando o valor for válido ou adicione um erro quando não for. O corpo do método pode chamar qualquer código de validação que a sua aplicação já usa, como App\Payment\CardNumber::hasValidChecksum($value).
Para uma lógica de validação reaproveitada em vários formulários, crie uma regra e um handler personalizados do Yii Validator em vez de copiar métodos de callback. Veja o guia de regras personalizadas para a estrutura de regra/handler.
Validando vários campos juntos
Atributos nas propriedades bastam quando cada campo pode ser validado de forma independente. Quando uma regra depende de vários campos, coloque uma regra Callback na classe do form model e associe o erro ao campo que deve exibi-lo.
Por exemplo, um filtro de relatório pode exigir que a data inicial seja anterior ou igual à data final:
<?php
declare(strict_types=1);
namespace App\Web\Report;
use DateTimeImmutable;
use Yiisoft\FormModel\FormModel;
use Yiisoft\Validator\Result;
use Yiisoft\Validator\Rule\Callback;
use Yiisoft\Validator\Rule\Date\Date;
use Yiisoft\Validator\Rule\Required;
use Yiisoft\Validator\ValidationContext;
#[Callback(method: 'validateDateRange')]
final class ReportFilterForm extends FormModel
{
#[Required]
#[Date(format: 'php:Y-m-d')]
public string $dateFrom = '';
#[Required]
#[Date(format: 'php:Y-m-d')]
public string $dateTo = '';
public function validateDateRange(mixed $value, Callback $rule, ValidationContext $context): Result
{
$result = new Result();
if ($this->dateFrom === '' || $this->dateTo === '') {
return $result;
}
$dateFrom = $this->parseDate($this->dateFrom);
$dateTo = $this->parseDate($this->dateTo);
if ($dateFrom === null || $dateTo === null) {
return $result;
}
if ($dateFrom > $dateTo) {
$result->addError('The start date must be earlier than or equal to the end date.', [], ['dateFrom']);
}
return $result;
}
private function parseDate(string $value): ?DateTimeImmutable
{
$date = DateTimeImmutable::createFromFormat('!Y-m-d', $value);
$errors = DateTimeImmutable::getLastErrors();
if ($date === false || ($errors !== false && ($errors['warning_count'] > 0 || $errors['error_count'] > 0))) {
return null;
}
return $date;
}
}O Callback no nível da classe recebe o objeto inteiro como $value, mas usar $this costuma ser mais claro em um método do form model. Retornar um Result vazio significa que a regra entre campos passou. O terceiro argumento de addError() é o caminho do valor; ['dateFrom'] faz o erro aparecer no campo dateFrom quando o formulário é renderizado.
Usando o formulário
Agora que você tem um formulário, use-o na sua action do "Dizendo olá".
Este é o resultado final em /src/Web/Echo/Action.php:
<?php
declare(strict_types=1);
namespace App\Web\Echo;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Yiisoft\FormModel\FormHydrator;
use Yiisoft\Yii\View\Renderer\WebViewRenderer;
final readonly class Action
{
public function __construct(
private WebViewRenderer $viewRenderer,
private FormHydrator $formHydrator,
) {}
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
$form = new Form();
$this->formHydrator->populateFromPostAndValidate($form, $request);
return $this->viewRenderer->render(__DIR__ . '/template', [
'form' => $form,
]);
}
}Em vez de ler da rota, você preenche o formulário com os dados POST da requisição e o valida com a ajuda do FormHydrator. Depois, passa o formulário para a view.
Para o formulário funcionar, precisamos permitir tanto GET, para renderizar o formulário, quanto POST, para enviar os dados. Ajuste a sua rota em config/common/routes.php:
<?php
declare(strict_types=1);
use App\Web;
use Yiisoft\Http\Method;
use Yiisoft\Router\Group;
use Yiisoft\Router\Route;
return [
Group::create()
->routes(
Route::get('/')
->action(Web\HomePage\Action::class)
->name('home'),
Route::methods([Method::GET, Method::POST], '/say')
->action(Web\Echo\Action::class)
->name('echo/say'),
),
];Ajustando a view
Para renderizar um formulário, você precisa alterar a sua view, src/Web/Echo/template.php:
<?php
use App\Web\Echo\Form;
use Yiisoft\FormModel\Field;
use Yiisoft\Html\Html;
use Yiisoft\Router\UrlGeneratorInterface;
use Yiisoft\Yii\View\Renderer\Csrf;
/**
* @var Form $form
* @var string[] $errors
* @var UrlGeneratorInterface $urlGenerator
* @var Csrf $csrf
*/
$htmlForm = Html::form()
->post($urlGenerator->generate('echo/say'))
->csrf($csrf);
?>
<?= $htmlForm->open() ?>
<?= Field::text($form, 'message')->required() ?>
<?= Html::submitButton('Say') ?>
<?= $htmlForm->close() ?>
<?php if ($form->isValid()): ?>
Echo said: <?= Html::encode($form->message) ?>
<?php endif ?>Se o formulário for válido, você exibe uma mensagem. O restante inicializa e renderiza o formulário.
Primeiro, você inicializa $htmlForm com o tipo POST e a URL da action gerada com a ajuda do gerador de URLs. Você pode acessá-lo como $urlGenerator em todas as views. Também precisa passar o token CSRF para o formulário, que também está disponível em toda view como $csrf graças às view injections listadas em config/common/params.php:
'yiisoft/yii-view-renderer' => [
'injections' => [
Reference::to(CsrfViewInjection::class),
],
],O template renderiza o valor do token CSRF como um input oculto para garantir que a requisição vem da página do formulário e não de outro site. Ele é enviado junto com os dados POST do formulário. Sem ele, o resultado seria o código de resposta HTTP 422.
Você usa Field::text() para exibir o campo "message", então ele cuida de preencher o valor, escapá-lo e renderizar o label do campo e os erros de validação.
Agora, se você enviar uma mensagem vazia, vai receber um erro de validação: "The message to be echoed must contain at least 2 characters."
Testando
Para ver como funciona, acesse a seguinte URL no navegador:
http://localhost:8080/sayVocê vai ver uma página com um campo de formulário e um label indicando qual dado informar. O formulário também tem um botão "submit" com o texto "Say". Se você clicar no botão "submit" sem digitar nada, vai ver que o campo é obrigatório. Se digitar um único caractere, o formulário mostra uma mensagem de erro ao lado do campo com problema.

Depois de digitar uma mensagem válida e clicar no botão "submit", a página mostra de volta os dados que você digitou.

Resumo
Nesta seção do guia, você aprendeu a criar uma classe de form model para representar os dados do usuário e validar esses dados.
Você também aprendeu a receber dados dos usuários e a exibi-los de volta no navegador. Essa é uma tarefa que pode tomar muito tempo no desenvolvimento de uma aplicação, mas o Yii oferece widgets poderosos que a tornam fácil.
Na próxima seção, você vai aprender a trabalhar com bancos de dados, que são necessários em quase toda aplicação.