Data e hora em PHP: formatar, fusos e DateTimeImmutable

Obter e formatar uma data em PHP: definir o fuso horário, formatar com date(), calcular com DateTimeImmutable, os nomes de dias e de meses em francês com IntlDateFormatter e as funções obsoletas que convém abandonar.

Data e hora em PHP: formatar, fusos e DateTimeImmutable
Resposta rápida

date() formata um timestamp, DateTimeImmutable serve assim que há um cálculo e IntlDateFormatter devolve os nomes de dias e de meses em francês. Define o fuso de forma explícita: sem date.timezone, o PHP usa UTC. strftime() está obsoleta desde o PHP 8.1 e o seu substituto não é date(), que não traduz nada.

Mostrar uma data em PHP é simples. Mostrar a data certa, no fuso horário certo, em francês, e calcular um intervalo sem te enganares na mudança da hora já nem por isso. Este artigo cobre as funções de base e depois as classes DateTimeImmutable e IntlDateFormatter, que resolvem os casos que date() não sabe tratar. Todos os resultados foram obtidos em PHP 8.5.10.

O fuso horário, antes de tudo o resto

É a fonte de erros número um, e é silenciosa. Se date.timezone não estiver definido no php.ini, o PHP usa UTC. Um site francês mostra então uma hora desfasada de uma ou duas horas conforme a estação, sem o menor aviso.

php
echo ini_get('date.timezone'), "\n";        // (vazio) em muitas instalações
echo date_default_timezone_get(), "\n";     // UTC

A definição faz-se no php.ini, ou logo no arranque da aplicação:

php
date_default_timezone_set('Europe/Paris');

A regra de fundo: guardar em UTC, mostrar no fuso do utilizador. Uma base de dados que contém horas locais fica inutilizável assim que um utilizador muda de fuso ou entra a hora de verão.

date() e time()

time() devolve o número de segundos decorridos desde 1.º de janeiro de 1970 à meia-noite UTC. date() formata um timestamp desses, ou o instante atual se não lhe passares nenhum.

php
date_default_timezone_set('Europe/Paris');

echo date('d/m/Y'), "\n";           // 02/09/2026
echo date('d/m/Y H:i:s'), "\n";     // 02/09/2026 21:47:03
echo time(), "\n";                  // 1788378423

echo date('d/m/Y', 1645303037), "\n";  // 19/02/2022

Os caracteres de formato mais usados:

Data Hora
d dia com 2 dígitos (01 a 31) H hora em formato 24 h (00 a 23)
j dia sem zero à esquerda (1 a 31) h hora em formato 12 h (01 a 12)
m mês com 2 dígitos (01 a 12) i minutos (00 a 59)
n mês sem zero à esquerda (1 a 12) s segundos (00 a 59)
Y ano com 4 dígitos A AM ou PM
N dia da semana (1 = segunda-feira) P desvio, por exemplo +02:00
t número de dias do mês U timestamp Unix
L 1 se o ano for bissexto c formato ISO 8601 completo

Para inserir uma letra que também é um código de formato, tens de a escapar com uma barra invertida:

php
echo date('\L\e d/m/Y'), "\n";      // Le 02/09/2026
// sem a barra invertida, «L» daria 0 ou 1 e «e» o nome do fuso horário

date() não fala francês

É a segunda armadilha clássica. Os nomes de dias e de meses devolvidos por date() estão sempre em inglês, e setlocale() não muda nada.

php
$ts = mktime(0, 0, 0, 4, 1, 2022);

echo date('l', $ts), "\n";                    // Friday
setlocale(LC_TIME, 'fr_FR.UTF-8', 'fr_FR');
echo date('l', $ts), "\n";                    // Friday — sem alteração

A solução é o IntlDateFormatter, da extensão intl:

php
$formateur = new IntlDateFormatter(
    'fr_FR',
    IntlDateFormatter::FULL,
    IntlDateFormatter::NONE,
    'Europe/Paris',
    IntlDateFormatter::GREGORIAN,
);

echo $formateur->format($ts), "\n";   // vendredi 1 avril 2022

Para um formato à medida, a classe aceita um padrão ICU, cujos códigos não são os de date():

php
$formateur->setPattern("EEEE d MMMM y '\u{e0}' HH'h'mm");
echo $formateur->format(new DateTimeImmutable('now', new DateTimeZone('Europe/Paris')));
// mercredi 2 septembre 2026 à 21h47

Se o intl não estiver disponível, uma tabela de correspondência safa-te em francês, mas não escala para várias línguas.

DateTimeImmutable, a classe a usar

Para tudo o que vai além da apresentação, as classes de data valem mais do que as funções. DateTimeImmutable é preferível a DateTime: os seus métodos devolvem um novo objeto em vez de alterarem o objeto atual, o que elimina as modificações à distância.

php
$paris = new DateTimeZone('Europe/Paris');
$date  = new DateTimeImmutable('2026-09-02 14:30:00', $paris);

echo $date->format('d/m/Y H:i P'), "\n";                     // 02/09/2026 14:30 +02:00
echo $date->modify('+3 days')->format('d/m/Y'), "\n";        // 05/09/2026
echo $date->format('d/m/Y'), "\n";                           // 02/09/2026 — sem alteração

// Com DateTime (mutável), a terceira linha mostraria 05/09/2026
php
// Acrescentar e retirar durações
$dans30Mois = $date->add(new DateInterval('P30M'));
$ilYa2Sem   = $date->sub(new DateInterval('P2W'));

// Diferença entre duas datas
$ecart = $date->diff(new DateTimeImmutable('2026-12-25', $paris));
echo $ecart->days, " jours\n";
echo $ecart->format('%m mois et %d jours'), "\n";

// Uma série de datas
$periode = new DatePeriod(
    $date,
    new DateInterval('P1D'),
    new DateTimeImmutable('2026-09-07', $paris),
);
foreach ($periode as $jour) {
    echo $jour->format('D d/m'), "\n";
}

A armadilha do último dia do mês

Somar um mês a um fim de mês não dá aquilo que esperas, e o comportamento é o mesmo com DateInterval e com a aritmética sobre mktime().

php
$fin = new DateTimeImmutable('2026-01-31');
echo $fin->add(new DateInterval('P1M'))->format('d/m/Y'), "\n";  // 03/03/2026

O 31 de fevereiro não existe: o PHP deixa-o transbordar para março. Para obter o último dia do mês seguinte, é preciso pedi-lo de forma explícita:

php
echo $fin->modify('last day of next month')->format('d/m/Y'), "\n";  // 28/02/2026

Um dia nem sempre tem 24 horas

Na passagem à hora de verão, um dia tem 23, na hora de inverno, tem 25. Somar 86400 segundos a um timestamp dá então uma hora errada.

php
$paris = new DateTimeZone('Europe/Paris');
$avant = new DateTimeImmutable('2026-03-28 12:00:00', $paris);
$apres = $avant->add(new DateInterval('P1D'));

echo $apres->format('d/m/Y H:i P'), "\n";   // 29/03/2026 12:00 +02:00
echo ($apres->getTimestamp() - $avant->getTimestamp()) / 3600, " heures\n";  // 23

DateInterval raciocina em dias de calendário e devolve mesmo o meio-dia seguinte, embora só tenham passado 23 horas. É o comportamento correto para um lembrete «amanhã ao meio-dia», e o errado para uma duração faturada. São duas noções distintas: P1D para um dia de calendário, PT24H para vinte e quatro horas.

Ler uma data escrita pelo utilizador

strtotime() percebe muita coisa, mas interpreta os formatos ambíguos à americana: 03/04/2026 passa a 4 de março, e não 3 de abril. Para um formato conhecido, createFromFormat() não deixa margem para dúvidas.

php
$date = DateTimeImmutable::createFromFormat(
    '!d/m/Y',                        // o «!» repõe a hora a 00:00:00
    '03/04/2026',
    new DateTimeZone('Europe/Paris'),
);
echo $date->format('d F Y'), "\n";   // 03 April 2026

Sem o ! inicial, os campos não fornecidos ficam com o valor do instante atual: duas execuções do mesmo código dão duas horas diferentes.

O ponto mais perigoso: uma data inválida não falha, transborda em silêncio.

php
$saisie = '31/02/2026';
$date   = DateTimeImmutable::createFromFormat('!d/m/Y', $saisie, $paris);

echo $date->format('d/m/Y'), "\n";                    // 03/03/2026
print_r(DateTimeImmutable::getLastErrors());          // ['warnings' => [10 => 'The parsed date was invalid'], …]
var_dump(checkdate(2, 31, 2026));                     // bool(false)

É preciso, portanto, verificar getLastErrors() depois de cada análise, ou comparar a data reformatada com aquilo que foi escrito:

php
function lireDate(string $saisie, DateTimeZone $fuseau): ?DateTimeImmutable
{
    $date = DateTimeImmutable::createFromFormat('!d/m/Y', $saisie, $fuseau);

    if ($date === false) {
        return null;
    }

    $erreurs = DateTimeImmutable::getLastErrors();
    if ($erreurs !== false && ($erreurs['warning_count'] || $erreurs['error_count'])) {
        return null;
    }

    return $date->format('d/m/Y') === $saisie ? $date : null;
}

Desde o PHP 8.2, getLastErrors() devolve false quando não há nada a assinalar, em vez de um array com contadores a zero. O teste tem de contar com isso.

Novidades recentes

O PHP 8.4 acrescentou createFromTimestamp(), que aceita timestamps fracionários:

php
$d = DateTimeImmutable::createFromTimestamp(1645303037.5);
echo $d->format('d/m/Y H:i:s.u P'), "\n";   // 19/02/2022 20:37:17.500000 +00:00

Antes era preciso passar por '@' . $ts ou setTimestamp(), e os microssegundos perdiam-se.

As funções obsoletas que já não deves usar

Várias funções de data ainda presentes emitem um aviso de obsolescência. Funcionam em PHP 8.5.10, mas a sua remoção já está marcada.

Função Obsoleta desde Substituto
strftime() PHP 8.1 IntlDateFormatter::format()
gmstrftime() PHP 8.1 IntlDateFormatter::format()
strptime() PHP 8.2 date_parse_from_format() ou IntlDateFormatter::parse()
date_sunrise() / date_sunset() PHP 8.1 date_sun_info()
utf8_encode() PHP 8.2 mb_convert_encoding()

As mensagens exatas, recolhidas em PHP 8.5.10 tal como em PHP 8.4.25:

code
Deprecated: Function strftime() is deprecated since 8.1,
use IntlDateFormatter::format() instead

Deprecated: Function strptime() is deprecated since 8.2,
use date_parse_from_format() (for locale-independent parsing),
or IntlDateFormatter::parse() (for locale-dependent parsing) instead

Deprecated: Constant SUNFUNCS_RET_STRING is deprecated since 8.4,
as date_sunrise() and date_sunset() were deprecated in 8.1

strftime() era a forma habitual de obter uma data em francês. O seu substituto não é date(), que não traduz nada, mas sim IntlDateFormatter.

Pelo contrário, mktime(), checkdate(), getdate() e idate() não estão obsoletas e funcionam normalmente. Continuam úteis para casos simples, mesmo que as classes cubram tudo o que elas fazem.

A reter

  • Define o fuso de forma explícita, guarda em UTC, mostra em hora local.
  • date() para formatar um instante, DateTimeImmutable para qualquer cálculo.
  • IntlDateFormatter para os nomes de dias e de meses em francês.
  • Depois de createFromFormat(), verifica getLastErrors(): uma data inválida transborda em silêncio.
  • Um dia de calendário não são 24 horas. P1D e PT24H não são intercambiáveis.

Para analisar datas aos milhões num ficheiro de importação, vê ler ficheiros grandes com PHP, e a ligação a uma base de dados para as guardares em UTC.

Vê também validar uma data com uma expressão regular, gerir datas com Carbon no Laravel e o hub Desenvolvimento web.

Erros frequentes

Fuso horário por definir Sem date.timezone no php.ini, o PHP cai em UTC e mostra uma hora desfasada sem o menor aviso.
date('l') continua em inglês setlocale() não tem qualquer efeito sobre date(). O substituto de strftime() é IntlDateFormatter, não date().
createFromFormat aceita uma data inválida 31/02/2026 passa a 03/03/2026 com um simples aviso em getLastErrors(). Tens de o verificar depois de cada análise.
createFromFormat sem o «!» Os campos não fornecidos ficam com a hora atual: duas execuções do mesmo código dão dois resultados.
strtotime lê as datas à americana 03/04/2026 é 4 de março, não 3 de abril.
Somar um mês a um fim de mês 31/01/2026 + P1M03/03/2026. Para o último dia do mês, usa modify('last day of next month').
Um dia não tem 24 horas Na mudança da hora, P1D avança mesmo um dia de calendário embora só tenham passado 23 horas. P1D e PT24H não são intercambiáveis.
getLastErrors devolve false Desde o PHP 8.2, o método devolve false quando não há nada a assinalar, em vez de um array de contadores a zero.

DateDateTimeIntlPHP

Damien Flandrin Programador web desde 2010, criador da Gekkode e do Email Impact. Cada artigo é testado num projeto real antes de ser publicado. Contacto
Newsletter

Os novos testes, tutoriais e projetos, por e-mail.

Testes reproduzíveis, código versionado, resultados datados. Nunca spam.