Ordenação
Deixar o usuário ordenar uma lista clicando no cabeçalho da coluna parece simples, mas tem dois cuidados: o valor vem da URL, então não pode virar SQL sem controle, e às vezes uma "coluna" da tela corresponde a mais de um campo no banco. A classe Sort, do yiisoft/data, cuida dos dois. Este capítulo mostra como configurá-la, como ler a ordem da URL e como montar os links.
A classe Sort
Um Sort guarda duas coisas:
- A configuração: quais campos podem ser ordenados e, para cada um, o que "crescente" e "decrescente" significam.
- A ordem atual: os campos que o usuário escolheu, com a direção.
A partir delas, o método getCriteria() produz a lista final de campos e direções que vai para o data reader.
Existem duas formas de criar:
Sort::only([...])aceita só os campos da configuração. Um campo desconhecido vindo do usuário é ignorado.Sort::any([...])aceita qualquer campo. Os que não estão na configuração passam como vieram.
Para ordenação que vem da URL, use sempre only(). Com any(), o texto do usuário chega ao ORDER BY. Um nome simples, como senha, vira ORDER BY "senha", entre aspas. Mas o query builder trata qualquer coisa com parênteses como expressão e não põe aspas: com Sort::any(), o valor (SELECT 1) vira ORDER BY (SELECT 1) e é executado. Isso é uma porta aberta para SQL injection.
Configurando os campos
A configuração é um array. Um campo simples é só o nome. Um campo com regras próprias é uma chave com as opções asc, desc e default:
$sort = Sort::only([
'titulo',
'autor' => [
'asc' => ['autor' => SORT_ASC, 'ano' => SORT_ASC],
'desc' => ['autor' => SORT_DESC, 'ano' => SORT_ASC],
],
'ano',
'preco' => ['default' => 'desc'],
]);tituloeanoordenam por eles mesmos, em qualquer direção.autoré um campo lógico: ordenar por autor ordena pelo nome e, dentro do mesmo autor, pelo ano, sempre do mais antigo para o mais novo. As chavesascedescdizem quais campos reais usar em cada direção.precotem a direção padrão invertida: o primeiro clique mostra os mais caros. Semdefault, a direção padrão éasc.
Os nomes dentro de asc e desc são os campos que o data reader conhece. Com o QueryDataReader, são as colunas da consulta.
Lendo a ordem da URL
A convenção do yiisoft/data é uma string com os campos separados por vírgula, e um - na frente para decrescente: preco, -preco, -autor,titulo. O método withOrderString() interpreta essa string, e getOrderAsString() faz o caminho inverso. Para montar a ordem a partir de um array, use withOrder(['preco' => 'desc']).
Veja o que getCriteria() produz com a configuração acima (SORT_ASC é 4 e SORT_DESC é 3):
'' {"titulo":4,"autor":4,"ano":4,"preco":3}
'-preco' {"preco":3,"titulo":4,"autor":4,"ano":4}
'autor' {"autor":4,"ano":4,"titulo":4,"preco":3}
'-autor,titulo' {"autor":3,"ano":4,"titulo":4,"preco":3}
'senha' {"titulo":4,"autor":4,"ano":4,"preco":3}Dois comportamentos aparecem aqui:
- Depois dos campos escolhidos, o
Sortacrescenta os outros campos da configuração, cada um na sua direção padrão. Eles funcionam como critério de desempate e garantem uma ordem estável. Sem nada na URL, vale só essa ordem padrão, que começa pelo título. Se você não quer esse acréscimo, chamewithoutDefaultSorting(): aí'-preco'produz só{"preco":3}. senhanão está na configuração e foi descartado.
A página
Crie src/Web/Dados/Ordenacao/Action.php:
<?php
declare(strict_types=1);
namespace App\Web\Dados\Ordenacao;
use Psr\Http\Message\ResponseInterface;
use Yiisoft\Data\Db\QueryDataReader;
use Yiisoft\Data\Reader\Sort;
use Yiisoft\Db\Connection\ConnectionInterface;
use Yiisoft\Input\Http\Attribute\Parameter\Query;
use Yiisoft\Yii\View\Renderer\WebViewRenderer;
final readonly class Action
{
public function __construct(
private WebViewRenderer $viewRenderer,
private ConnectionInterface $db,
) {}
public function __invoke(#[Query('ordem')] string $ordem = ''): ResponseInterface
{
$query = $this->db
->select(['titulo' => 'l.titulo', 'autor' => 'a.nome', 'ano' => 'l.ano', 'preco' => 'l.preco'])
->from(['l' => 'livro'])
->innerJoin(['a' => 'autor'], 'a.id = l.autor_id');
$sort = Sort::only([
'titulo',
'autor' => [
'asc' => ['autor' => SORT_ASC, 'ano' => SORT_ASC],
'desc' => ['autor' => SORT_DESC, 'ano' => SORT_ASC],
],
'ano',
'preco' => ['default' => 'desc'],
])->withOrderString($ordem);
$leitor = (new QueryDataReader($query))->withSort($sort);
return $this->viewRenderer->render(__DIR__ . '/template', [
'sort' => $sort,
'livros' => $leitor->read(),
]);
}
}A consulta não tem orderBy(). Quem ordena é o data reader: withSort() devolve um novo leitor, que transforma os critérios em ORDER BY na hora de ler. Se a consulta já tivesse um orderBy(), os critérios do Sort seriam acrescentados depois dele, e a escolha do usuário teria efeito só no desempate.
As colunas da consulta têm alias ('autor' => 'a.nome'), e o Sort usa esses aliases. Com ?ordem=-preco, o SQL fica:
SELECT "l"."titulo" AS "titulo", "a"."nome" AS "autor", "l"."ano" AS "ano", "l"."preco" AS "preco"
FROM "livro" "l" INNER JOIN "autor" "a" ON a.id = l.autor_id ORDER BY "preco" DESC, "titulo", "autor", "ano"Usar o alias funciona no ORDER BY em qualquer banco, mas no WHERE não: MySQL e PostgreSQL recusam (o SQLite aceita, como extensão própria). Quando o mesmo leitor também for filtrado, mapeie os nomes para as colunas reais com o fieldMapper, mostrado no capítulo Data readers.
Links no cabeçalho
Cada cabeçalho é um link que ordena pela coluna. Se a lista já está ordenada por ela, o link inverte a direção; se não está, usa a direção padrão do campo. A view, em src/Web/Dados/Ordenacao/template.php:
<?php
declare(strict_types=1);
use Yiisoft\Html\Html;
/**
* @var Yiisoft\View\WebView $this
* @var Yiisoft\Router\UrlGeneratorInterface $urlGenerator
* @var Yiisoft\Data\Reader\Sort $sort
* @var iterable<array> $livros
*/
$this->setTitle('Livros ordenados');
$cabecalho = static function (string $campo, string $rotulo) use ($sort, $urlGenerator): string {
$atual = $sort->getOrder()[$campo] ?? null;
$proxima = match ($atual) {
'asc' => 'desc',
'desc' => 'asc',
null => $sort->getDefaultOrder()[$campo],
};
$url = $urlGenerator->generate('dados/ordenacao', queryParameters: [
'ordem' => ($proxima === 'desc' ? '-' : '') . $campo,
]);
return Html::a($rotulo . ($atual === null ? '' : " ($atual)"), $url)->render();
};
?>
<h1>Livros ordenados</h1>
<table>
<tr>
<th><?= $cabecalho('titulo', 'Título') ?></th>
<th><?= $cabecalho('autor', 'Autor') ?></th>
<th><?= $cabecalho('ano', 'Ano') ?></th>
<th><?= $cabecalho('preco', 'Preço') ?></th>
</tr>
<?php foreach ($livros as $livro): ?>
<tr>
<td><?= Html::encode($livro['titulo']) ?></td>
<td><?= Html::encode($livro['autor']) ?></td>
<td><?= Html::encode($livro['ano']) ?></td>
<td><?= Html::encode($livro['preco']) ?></td>
</tr>
<?php endforeach ?>
</table>getOrder() devolve só a ordem escolhida pelo usuário, como ['preco' => 'desc'], sem os campos de desempate. getDefaultOrder() devolve a direção padrão de cada campo configurado.
A rota:
Route::get('/dados/ordenacao')
->action(Web\Dados\Ordenacao\Action::class)
->name('dados/ordenacao'),Testando:
/dados/ordenacaolista por título, de "A Hora da Estrela" a "Vidas Secas". O link do cabeçalho "Preço" aponta para?ordem=-preco, por causa dodefault./dados/ordenacao?ordem=-precocomeça por "Dona Flor e Seus Dois Maridos" (74.9) e mostra o cabeçalho "Preço (desc)", que agora aponta para?ordem=preco./dados/ordenacao?ordem=autoragrupa por autor, começando por Clarice Lispector, com os livros de cada um em ordem de ano./dados/ordenacao?ordem=senhaignora o campo e mostra a ordem padrão.
Ordenação e paginação juntas
O paginador também aceita um Sort: $paginador->withSort($sort) repassa para o data reader. Ao montar os links de página, inclua o parâmetro ordem atual ($sort->getOrderAsString()), senão a ordenação se perde ao trocar de página. E ao trocar a ordenação, volte para a página 1: a página 3 de uma ordem não tem relação com a página 3 de outra.
O GridView, do capítulo Widgets de dados, faz tudo isso sozinho: gera os links de cabeçalho, mantém a ordem na paginação e lê o parâmetro sort da URL.
Comparando com o Yii2
- O
yii\data\Sortdo Yii2 tinha a mesma ideia de atributos comasc,descedefault, e também usava-para decrescente na URL. A diferença é que ele lia a requisição sozinho; oSortdo Yii3 recebe a string, e quem lê a URL é a action. - O
Sortdo Yii2 gerava os links ($sort->link('nome')). No Yii3, a view monta os links, ou oGridViewfaz isso.