Requisição
Toda action precisa de dados que vêm do cliente: parâmetros da URL, campos de formulário, JSON, headers. No Yii3 esses dados chegam num objeto PSR-7, e você pode ler direto dele ou deixar que atributos e DTOs façam a extração. Este capítulo mostra as duas formas.
ServerRequestInterface
A requisição é um objeto que implementa Psr\Http\Message\ServerRequestInterface, da PSR-7. O Yii3 não tem uma classe Request própria: o template usa a implementação do pacote httpsoft/http-message, mas o seu código só depende da interface.
Para receber a requisição, declare um parâmetro com esse tipo no método da action. O dispatcher de middlewares entrega o objeto. Crie src/Web/Requisicoes/Inspecao/Action.php:
<?php
declare(strict_types=1);
namespace App\Web\Requisicoes\Inspecao;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
final readonly class Action
{
public function __construct(
private ResponseFactoryInterface $responseFactory,
) {}
public function __invoke(ServerRequestInterface $request): ResponseInterface
{
$linhas = [
'Método: ' . $request->getMethod(),
'Caminho: ' . $request->getUri()->getPath(),
'Query: ' . json_encode($request->getQueryParams(), JSON_UNESCAPED_UNICODE),
'Content-Type: ' . $request->getHeaderLine('Content-Type'),
'User-Agent: ' . $request->getHeaderLine('User-Agent'),
'Corpo: ' . json_encode($request->getParsedBody(), JSON_UNESCAPED_UNICODE),
];
$response = $this->responseFactory->createResponse();
$response->getBody()->write(implode("\n", $linhas) . "\n");
return $response->withHeader('Content-Type', 'text/plain; charset=utf-8');
}
}E a rota, aceitando GET e POST:
use Yiisoft\Http\Method;
use Yiisoft\Request\Body\RequestBodyParser;
Route::methods([Method::GET, Method::POST], '/inspecao')
->middleware(RequestBodyParser::class)
->action(Requisicoes\Inspecao\Action::class)
->name('inspecao'),O RequestBodyParser é explicado mais abaixo. Os métodos mais usados da requisição:
getMethod(): o verbo, comoGETouPOST.getUri(): umUriInterface, comgetPath(),getQuery(),getHost()e outros.getQueryParams(): a query string como array, o equivalente a$_GET.getParsedBody(): o corpo já interpretado, o equivalente a$_POST.getHeaderLine('Nome'): o valor do header como string, vazia se ele não existir. OgetHeader('Nome')devolve os valores como array.getCookieParams(): os cookies, o equivalente a$_COOKIE.getUploadedFiles(): os arquivos enviados, como objetosUploadedFileInterface.getAttribute('nome'): valores que middlewares penduraram na requisição.getServerParams(): o equivalente a$_SERVER.
Nomes de header não diferenciam maiúsculas de minúsculas: getHeaderLine('content-type') funciona igual.
Uma requisição GET com query string:
curl "http://127.0.0.1:8080/requisicoes/inspecao?ordem=nome&pagina=2"Método: GET
Caminho: /requisicoes/inspecao
Query: {"ordem":"nome","pagina":"2"}
Content-Type:
User-Agent: curl/8.21.0
Corpo: nullOs valores da query string são sempre strings. Converter e validar é responsabilidade sua, ou dos atributos que aparecem adiante.
POST e o token CSRF
O template coloca o CsrfTokenMiddleware na pilha global, então todo POST, PUT, PATCH ou DELETE sem um token CSRF válido é recusado com 422. Num formulário HTML, o ->csrf($csrf) do Html::form() resolve isso, como no capítulo Trabalhando com formulários.
Para testar com o curl, é preciso guardar o cookie de sessão e mandar o token no header X-CSRF-Token. O layout do template publica o token numa tag <meta name="csrf">:
TOKEN=$(curl -s -c cookies.txt http://127.0.0.1:8080/ | sed -n 's/.*name="csrf" content="\([^"]*\)".*/\1/p')
curl -b cookies.txt -H "X-CSRF-Token: $TOKEN" -d "nome=Ana&idade=30" \
http://127.0.0.1:8080/requisicoes/inspecaoMétodo: POST
Caminho: /requisicoes/inspecao
Query: []
Content-Type: application/x-www-form-urlencoded
User-Agent: curl/8.21.0
Corpo: {"nome":"Ana","idade":"30"}Os exemplos de POST deste capítulo usam esse $TOKEN e esse cookies.txt. Uma API que recebe chamadas de outros sistemas normalmente não usa CSRF; isso é tratado na seção APIs REST.
Corpo em JSON
O getParsedBody() só vem preenchido sozinho para formulários (application/x-www-form-urlencoded e multipart/form-data). Para JSON, o corpo fica cru e getParsedBody() devolve null.
O middleware RequestBodyParser, do pacote yiisoft/request-body-parser, resolve isso: ele lê o Content-Type, decodifica o JSON e coloca o resultado em getParsedBody(). Por isso ele está na rota de /inspecao:
curl -b cookies.txt -H "X-CSRF-Token: $TOKEN" -H "Content-Type: application/json" \
-d '{"nome":"Ana","tags":["php","yii"]}' \
http://127.0.0.1:8080/requisicoes/inspecaoMétodo: POST
Caminho: /requisicoes/inspecao
Query: []
Content-Type: application/json
User-Agent: curl/8.21.0
Corpo: {"nome":"Ana","tags":["php","yii"]}Sem o middleware na rota, a mesma chamada mostra Corpo: null. Com JSON malformado, o próprio middleware responde 400 Bad Request e a action nem é chamada.
Aqui o middleware foi colocado só numa rota. Se a aplicação inteira recebe JSON, ele pode ir para um grupo ou para a pilha global; veja Middleware.
Atributos de entrada
Ler getQueryParams()['pagina'] e converter na mão fica repetitivo. O pacote yiisoft/input-http traz atributos que preenchem parâmetros da action a partir da requisição. Você já viu o #[Query] no capítulo Dizendo "Olá". Crie src/Web/Requisicoes/Busca/Action.php:
<?php
declare(strict_types=1);
namespace App\Web\Requisicoes\Busca;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Yiisoft\Input\Http\Attribute\Parameter\Query;
final readonly class Action
{
public function __construct(
private ResponseFactoryInterface $responseFactory,
) {}
public function __invoke(
#[Query('q')] string $termo = '',
#[Query] int $pagina = 1,
#[Query('tag')] array $tags = [],
): ResponseInterface {
$response = $this->responseFactory->createResponse();
$response->getBody()->write(sprintf(
"Termo: %s\nPágina: %d\nTags: %s\n",
$termo,
$pagina,
implode(', ', $tags),
));
return $response->withHeader('Content-Type', 'text/plain; charset=utf-8');
}
}curl "http://127.0.0.1:8080/requisicoes/busca?q=yii3&pagina=2&tag[]=php&tag[]=psr"Termo: yii3
Página: 2
Tags: php, psr- Sem argumento, o atributo usa o nome do parâmetro do método (
#[Query] int $paginalêpagina). - O valor é convertido para o tipo declarado. Se o parâmetro não vier, vale o padrão do método.
- A conversão não é validação. Com
?pagina=abc, o$paginachega como0. Confira os limites antes de usar o valor.
Os atributos disponíveis:
#[Query]: query string.#[Body]: corpo interpretado, o mesmo degetParsedBody().#[Request]: atributos da requisição, os degetAttribute().#[UploadedFiles]: arquivos enviados.#[RouteArgument]: parâmetros da rota. Este vem doyiisoft/router, os outros ficam emYiisoft\Input\Http\Attribute\Parameter.
Os quatro do input-http também aceitam um caminho em array, para dados aninhados: #[Body(['endereco', 'cidade'])].
Quem faz esses atributos funcionarem é o ParametersResolverInterface, configurado no template em config/web/di/application.php com o HydratorAttributeParametersResolver e o RequestInputParametersResolver.
DTOs de entrada com o hydrator
Quando a action recebe vários campos, é melhor juntá-los numa classe. Uma classe que estende AbstractInput é preenchida pelo hydrator e validada antes de chegar na action. Crie src/Web/Requisicoes/Contato/ContatoInput.php:
<?php
declare(strict_types=1);
namespace App\Web\Requisicoes\Contato;
use Yiisoft\Input\Http\AbstractInput;
use Yiisoft\Input\Http\Attribute\Data\FromBody;
use Yiisoft\Validator\Rule\Email;
use Yiisoft\Validator\Rule\Length;
use Yiisoft\Validator\Rule\Required;
#[FromBody]
final class ContatoInput extends AbstractInput
{
#[Required(message: 'Informe o nome.')]
public string $nome = '';
#[Required(message: 'Informe o e-mail.')]
#[Email(message: 'E-mail inválido.', skipOnEmpty: true)]
public string $email = '';
#[Length(max: 200, greaterThanMaxMessage: 'A mensagem pode ter no máximo {max} caracteres.')]
public string $mensagem = '';
}O #[FromBody] na classe diz que as propriedades vêm do corpo da requisição. Existe também o #[FromQuery], para a query string. As regras são as mesmas do yiisoft/validator, vistas em Validação.
A action só declara o DTO como parâmetro. Crie src/Web/Requisicoes/Contato/Action.php:
<?php
declare(strict_types=1);
namespace App\Web\Requisicoes\Contato;
use Psr\Http\Message\ResponseFactoryInterface;
use Psr\Http\Message\ResponseInterface;
use Yiisoft\Http\Status;
final readonly class Action
{
public function __construct(
private ResponseFactoryInterface $responseFactory,
) {}
public function __invoke(ContatoInput $contato): ResponseInterface
{
$resultado = $contato->getValidationResult();
if (!$resultado->isValid()) {
$response = $this->responseFactory->createResponse(Status::UNPROCESSABLE_ENTITY);
foreach ($resultado->getErrorMessagesIndexedByProperty() as $campo => $mensagens) {
$response->getBody()->write($campo . ': ' . implode(' ', $mensagens) . "\n");
}
return $response->withHeader('Content-Type', 'text/plain; charset=utf-8');
}
$response = $this->responseFactory->createResponse();
$response->getBody()->write("Mensagem de {$contato->nome} <{$contato->email}> recebida.\n");
return $response->withHeader('Content-Type', 'text/plain; charset=utf-8');
}
}A rota:
Route::post('/contato')
->middleware(RequestBodyParser::class)
->action(Requisicoes\Contato\Action::class)
->name('contato'),Testando com dados inválidos e depois válidos:
curl -b cookies.txt -H "X-CSRF-Token: $TOKEN" -H "Content-Type: application/json" \
-d '{"nome":"","email":"ana@"}' http://127.0.0.1:8080/requisicoes/contato
curl -b cookies.txt -H "X-CSRF-Token: $TOKEN" -H "Content-Type: application/json" \
-d '{"nome":"Ana","email":"ana@exemplo.com","mensagem":"Oi"}' http://127.0.0.1:8080/requisicoes/contatonome: Informe o nome.
email: E-mail inválido.
Mensagem de Ana <ana@exemplo.com> recebida.A primeira resposta vem com status 422, a segunda com 200.
Por padrão, um DTO inválido ainda chega na action, e você decide o que fazer com o getValidationResult(). O pacote tem o parâmetro throwInputValidationException (em $params['yiisoft/input-http']['requestInputParametersResolver']) que, ligado, faz o resolver lançar InputValidationException em vez de chamar a action. Isso combina com uma camada de tratamento de erros que converta a exceção numa resposta, assunto do capítulo Tratamento de erros.
Para formulários HTML com widgets de campo, o caminho continua sendo o FormModel e o FormHydrator, de Criando formulários. O DTO com AbstractInput é mais adequado para entradas sem view, como JSON. Upload de arquivos tem capítulo próprio: Upload de arquivos.
Comparando com o Yii2
- O
Yii::$app->requestglobal vira oServerRequestInterfacerecebido como parâmetro. Não existe acesso estático à requisição. $request->get('pagina')e$request->post('nome')viramgetQueryParams()egetParsedBody(), ou os atributos#[Query]e#[Body].- No Yii2, o
JsonParserera configurado emrequest.parsers. No Yii3, o equivalente é o middlewareRequestBodyParser. - O objeto de requisição é imutável. Para "alterar" a requisição, um middleware cria uma cópia com
withAttribute()e passa adiante. O capítulo Imutabilidade explica o porquê.