Formatação
O banco devolve 34.9 e 2026-02-02 10:00:00. Na tela, o usuário espera R$ 34,90 e 02/02/2026, 10:00. Este capítulo mostra como fazer essa conversão com a extensão intl do PHP e como deixar a formatação disponível nas views.
O Yii3 não tem um componente de formatação como o Yii::$app->formatter do Yii2, e o template não traz nenhum pacote para isso. A extensão intl resolve: ela usa a biblioteca ICU, que conhece as regras de cada idioma e país. Confira se ela está ativa com php -m.
Números e moeda
A classe NumberFormatter recebe o locale e um estilo:
$moeda = new NumberFormatter('pt_BR', NumberFormatter::CURRENCY);
$moeda->formatCurrency(1234567.891, 'BRL'); // R$ 1.234.567,89
$moeda->formatCurrency(34.9, 'USD'); // US$ 34,90
(new NumberFormatter('en_US', NumberFormatter::CURRENCY))->formatCurrency(1234567.891, 'BRL');
// R$1,234,567.89
(new NumberFormatter('pt_BR', NumberFormatter::DECIMAL))->format(1440000000); // 1.440.000.000
(new NumberFormatter('pt_BR', NumberFormatter::PERCENT))->format(0.125); // 12%O locale define os separadores e a posição do símbolo; o código da moeda (BRL, USD, EUR) define qual símbolo usar. Preço em reais mostrado para um usuário americano continua sendo em reais, só muda a pontuação.
As casas decimais são controladas com setAttribute():
$numero = new NumberFormatter('pt_BR', NumberFormatter::DECIMAL);
$numero->setAttribute(NumberFormatter::FRACTION_DIGITS, 1);
$numero->format(68.2333); // 68,2WARNING
Entre R$ e o número, o ICU coloca um espaço não separável (U+00A0), e não um espaço comum. Na tela não faz diferença, mas um teste que compara com 'R$ 34,90' digitado no teclado falha.
Datas
Para datas, use IntlDateFormatter, com um estilo para a data e outro para a hora:
$data = new DateTimeImmutable('2026-02-02 10:00:00');
(new IntlDateFormatter('pt_BR', IntlDateFormatter::SHORT, IntlDateFormatter::NONE))->format($data);
// 02/02/2026
(new IntlDateFormatter('pt_BR', IntlDateFormatter::MEDIUM, IntlDateFormatter::NONE))->format($data);
// 2 de fev. de 2026
(new IntlDateFormatter('pt_BR', IntlDateFormatter::LONG, IntlDateFormatter::NONE))->format($data);
// 2 de fevereiro de 2026
(new IntlDateFormatter('pt_BR', IntlDateFormatter::FULL, IntlDateFormatter::NONE))->format($data);
// segunda-feira, 2 de fevereiro de 2026
(new IntlDateFormatter('pt_BR', IntlDateFormatter::SHORT, IntlDateFormatter::SHORT))->format($data);
// 02/02/2026, 10:00Se nenhum estilo serve, passe um padrão do ICU no sexto argumento. Ele não é o mesmo formato do date() do PHP: MMMM é o nome do mês e o texto fixo vai entre aspas simples.
(new IntlDateFormatter('pt_BR', IntlDateFormatter::NONE, IntlDateFormatter::NONE, null, null, "d 'de' MMMM 'de' y, HH:mm"))
->format($data);
// 2 de fevereiro de 2026, 10:00O quarto argumento é o fuso horário. Com null, vale o fuso padrão do PHP (date.timezone). Passando outro, a hora é convertida: com 'UTC', a mesma data acima sai como 02/02/2026, 13:00, porque o fuso padrão desta máquina é America/Sao_Paulo. Se o banco guarda datas em UTC e você quer exibir no horário do usuário, é aqui que a conversão acontece.
Um serviço de formatação
Criar os formatadores em toda view repete o locale e a moeda em vários lugares. Uma classe pequena resolve. Crie src/Dados/Formatador.php:
<?php
declare(strict_types=1);
namespace App\Dados;
use DateTimeImmutable;
use DateTimeInterface;
use IntlDateFormatter;
use NumberFormatter;
final readonly class Formatador
{
public function __construct(
private string $locale,
private string $moeda,
) {}
public function moeda(float|int|string $valor): string
{
$formatter = new NumberFormatter($this->locale, NumberFormatter::CURRENCY);
return $formatter->formatCurrency((float) $valor, $this->moeda);
}
public function numero(float|int|string $valor, int $casas = 0): string
{
$formatter = new NumberFormatter($this->locale, NumberFormatter::DECIMAL);
$formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $casas);
return $formatter->format((float) $valor);
}
public function percentual(float $valor, int $casas = 0): string
{
$formatter = new NumberFormatter($this->locale, NumberFormatter::PERCENT);
$formatter->setAttribute(NumberFormatter::FRACTION_DIGITS, $casas);
return $formatter->format($valor);
}
public function data(DateTimeInterface|string $valor, int $estilo = IntlDateFormatter::SHORT): string
{
$formatter = new IntlDateFormatter($this->locale, $estilo, IntlDateFormatter::NONE);
return $formatter->format($this->paraData($valor));
}
public function dataHora(DateTimeInterface|string $valor): string
{
$formatter = new IntlDateFormatter($this->locale, IntlDateFormatter::SHORT, IntlDateFormatter::SHORT);
return $formatter->format($this->paraData($valor));
}
private function paraData(DateTimeInterface|string $valor): DateTimeInterface
{
return $valor instanceof DateTimeInterface ? $valor : new DateTimeImmutable($valor);
}
}Os métodos aceitam string porque é assim que os valores chegam do banco: o preco, que é decimal, vem como '34.9', e o cadastrado_em vem como '2026-02-02 10:00:00'. A conversão para float acontece só no momento de formatar.
O locale e a moeda vêm da configuração. Em config/common/params/dados.php:
<?php
declare(strict_types=1);
return [
'dados' => [
'locale' => 'pt_BR',
'moeda' => 'BRL',
],
];E a definição no container, em config/common/di/dados.php:
<?php
declare(strict_types=1);
use App\Dados\Formatador;
/** @var array $params */
return [
Formatador::class => [
'__construct()' => [
'locale' => $params['dados']['locale'],
'moeda' => $params['dados']['moeda'],
],
],
];Os arquivos de definição recebem a variável $params com todos os parâmetros. O capítulo Container de DI explica a sintaxe '__construct()'. Num site com vários idiomas, o locale viria do usuário ou da requisição; o capítulo Internacionalização trata disso.
Formatando na view
A action recebe o Formatador pelo construtor e repassa para a view. Crie src/Web/Dados/Formatacao/Action.php:
<?php
declare(strict_types=1);
namespace App\Web\Dados\Formatacao;
use App\Dados\Formatador;
use Psr\Http\Message\ResponseInterface;
use Yiisoft\Db\Connection\ConnectionInterface;
use Yiisoft\Yii\View\Renderer\WebViewRenderer;
final readonly class Action
{
public function __construct(
private WebViewRenderer $viewRenderer,
private ConnectionInterface $db,
private Formatador $formatador,
) {}
public function __invoke(): ResponseInterface
{
$livros = $this->db
->select(['titulo', 'preco', 'cadastrado_em'])
->from('livro')
->orderBy(['cadastrado_em' => SORT_DESC])
->limit(5)
->all();
$total = $this->db->select()->from('livro')->sum('preco');
return $this->viewRenderer->render(__DIR__ . '/template', [
'livros' => $livros,
'total' => $total,
'formatador' => $this->formatador,
]);
}
}A view, src/Web/Dados/Formatacao/template.php, formata no momento de imprimir:
<?php
declare(strict_types=1);
use Yiisoft\Html\Html;
/**
* @var Yiisoft\View\WebView $this
* @var array[] $livros
* @var string|null $total
* @var App\Dados\Formatador $formatador
*/
$this->setTitle('Últimos cadastrados');
?>
<h1>Últimos cadastrados</h1>
<table>
<tr><th>Título</th><th>Preço</th><th>Com 15% de desconto</th><th>Cadastrado em</th></tr>
<?php foreach ($livros as $livro): ?>
<tr>
<td><?= Html::encode($livro['titulo']) ?></td>
<td><?= $formatador->moeda($livro['preco']) ?></td>
<td><?= $formatador->moeda($livro['preco'] * 0.85) ?></td>
<td><?= $formatador->dataHora($livro['cadastrado_em']) ?></td>
</tr>
<?php endforeach ?>
</table>
<p>
Valor do catálogo: <?= $formatador->moeda($total ?? 0) ?>.
Desconto aplicado: <?= $formatador->percentual(0.15) ?>.
Atualizado em <?= $formatador->data(new DateTimeImmutable(), IntlDateFormatter::LONG) ?>.
</p>Formatar na view, e não na action ou no banco, mantém os dados como dados até o último momento. A mesma lista pode virar uma página em português, um JSON para uma API ou uma planilha, cada um com o seu formato.
A saída dos números formatados não passa pelo Html::encode() porque vem do ICU, não do usuário. Já o título passa.
A rota:
Route::get('/dados/formatacao')
->action(Web\Dados\Formatacao\Action::class)
->name('dados/formatacao'),Em /dados/formatacao, a primeira linha da tabela é "Vidas Secas", com R$ 39,90, R$ 33,92 e 08/07/2026, 12:00. Abaixo da tabela aparece "Valor do catálogo: R$ 589,80. Desconto aplicado: 15%." e a data do dia por extenso, como "8 de outubro de 2026".
Se o formatador é usado em muitas views, você pode evitar repassá-lo em cada action, colocando-o nos parâmetros comuns de view do yiisoft/view (a chave parameters, a mesma que já entrega $urlGenerator). O capítulo Views mostra essa configuração.
Comparando com o Yii2
- O Yii2 tinha o componente
formatter, comasCurrency(),asDate(),asDecimal()e outros, usado comoYii::$app->formatter. O Yii3 não tem um equivalente pronto: você usa aintldireto ou escreve um serviço como oFormatador, injetado onde precisa. - O formatter do Yii2 aceitava formatos de data no estilo do PHP com o prefixo
php:. ComIntlDateFormatter, os padrões são sempre os do ICU.