Manipulação de datas com DateTime e DateTimeImmutable

1. Introdução às classes DateTime e DateTimeImmutable

Desde o PHP 5.2, o tratamento de datas ganhou duas classes poderosas: DateTime e DateTimeImmutable. A diferença fundamental entre elas está na mutabilidade: enquanto DateTime altera o próprio objeto ao modificar sua data, DateTimeImmutable retorna um novo objeto a cada modificação, preservando o original intacto.

Criar objetos é simples:

$mutavel = new DateTime();
$imutavel = new DateTimeImmutable();

// Com string personalizada
$natal = new DateTime('2024-12-25');
$anoNovo = new DateTimeImmutable('2025-01-01');

O construtor aceita diversos formatos de string, mas para evitar ambiguidades (como 02/03/2024 ser interpretado como 2 de março ou 3 de fevereiro), recomenda-se usar o formato ISO 8601 (YYYY-MM-DD) ou o formato americano (m/d/Y). Para maior controle, utilize DateTime::createFromFormat():

$data = DateTime::createFromFormat('d/m/Y', '31/12/2024');
if (!$data) {
    echo 'Formato inválido';
}

2. Métodos essenciais de formatação e exibição

O método format() é a principal ferramenta para exibir datas:

$agora = new DateTime();
echo $agora->format('d/m/Y H:i:s'); // 15/03/2025 14:30:00
echo $agora->format('l, F j, Y');   // Saturday, March 15, 2025

Caracteres de formatação comuns: d (dia com zero), m (mês numérico), Y (ano com 4 dígitos), H (hora 24h), i (minutos), s (segundos), l (nome do dia da semana), F (nome do mês).

Para localização, combine setlocale() com IntlDateFormatter (mais confiável que o antigo strftime):

$fmt = new IntlDateFormatter('pt_BR', IntlDateFormatter::LONG, IntlDateFormatter::NONE);
echo $fmt->format(new DateTime()); // 15 de março de 2025

Trabalhando com fusos horários:

$data = new DateTime('now', new DateTimeZone('America/Sao_Paulo'));
echo $data->format('H:i:s'); // Horário de Brasília

$data->setTimezone(new DateTimeZone('Asia/Tokyo'));
echo $data->format('H:i:s'); // Horário de Tóquio

Timezones comuns: America/Sao_Paulo, Europe/Lisbon, UTC, America/New_York. A lista completa está na documentação oficial de timezones do PHP.

3. Modificação de datas

Adicionando e subtraindo intervalos

$data = new DateTime('2024-01-15');
$data->add(new DateInterval('P10D')); // +10 dias
echo $data->format('Y-m-d'); // 2024-01-25

$data->sub(new DateInterval('P1M')); // -1 mês
echo $data->format('Y-m-d'); // 2023-12-25

Modificando componentes específicos

$data = new DateTime('2024-06-15 10:30:00');
$data->setDate(2025, 12, 31);
$data->setTime(23, 59, 59);
$data->setTimestamp(1704067199); // Timestamp Unix

O poderoso método modify()

$data = new DateTime('2024-01-01');
$data->modify('+1 month');
$data->modify('next monday');
$data->modify('last day of this month');

Expressões aceitas: +2 weeks, first day of January 2025, next thursday, last day of next month. Cuidado com ambiguidades: modify('+1 month') em 31 de janeiro resulta em 2 de março (devido ao fevereiro ter apenas 28 dias).

4. Comparação e diferença entre datas

Objetos DateTime podem ser comparados diretamente com operadores relacionais:

$inicio = new DateTime('2024-01-01');
$fim = new DateTime('2024-12-31');

var_dump($fim > $inicio);  // true
var_dump($inicio == $fim); // false

Para calcular diferenças, use diff():

$inicio = new DateTime('2024-01-15');
$fim = new DateTime('2025-03-20');
$intervalo = $inicio->diff($fim);

echo $intervalo->y . ' anos, ';  // 1 ano
echo $intervalo->m . ' meses, '; // 2 meses
echo $intervalo->d . ' dias';    // 5 dias
echo $intervalo->days;           // 430 dias (total)

O objeto DateInterval retornado possui propriedades: y, m, d, h, i, s, days (total de dias), invert (se a diferença é negativa) e format():

echo $intervalo->format('%y anos, %m meses e %d dias');
// 1 anos, 2 meses e 5 dias

5. DateInterval e DatePeriod para intervalos e períodos

Criando intervalos flexíveis

// Formato ISO 8601: P1Y2M10DT2H30M
$intervalo = new DateInterval('P1Y2M10DT2H30M');

// Ou a partir de string natural
$intervalo = DateInterval::createFromDateString('2 weeks + 3 days');

Iterando com DatePeriod

$inicio = new DateTime('2024-01-01');
$fim = new DateTime('2024-01-31');
$intervalo = new DateInterval('P1D');

$periodo = new DatePeriod($inicio, $intervalo, $fim);

foreach ($periodo as $dia) {
    echo $dia->format('d/m/Y') . ' - ' . $dia->format('l') . PHP_EOL;
}

Gerando apenas dias úteis:

$inicio = new DateTime('2024-01-01');
$periodo = new DatePeriod($inicio, new DateInterval('P1D'), new DateTime('2024-01-10'));

$diasUteis = [];
foreach ($periodo as $dia) {
    if ($dia->format('N') <= 5) { // 1=segunda, 5=sexta
        $diasUteis[] = $dia->format('d/m/Y');
    }
}
print_r($diasUteis);

6. Imutabilidade e segurança com DateTimeImmutable

Em aplicações modernas, especialmente com programação funcional ou concorrência, a imutabilidade é uma grande vantagem:

function calculaPrazo(DateTimeImmutable $dataBase): DateTimeImmutable {
    return $dataBase->modify('+30 days'); // Retorna NOVO objeto
}

$dataOriginal = new DateTimeImmutable('2024-01-01');
$novoPrazo = calculaPrazo($dataOriginal);

echo $dataOriginal->format('Y-m-d'); // 2024-01-01 (inalterada!)
echo $novoPrazo->format('Y-m-d');    // 2024-01-31

Com DateTime mutável, o mesmo código alteraria o objeto original:

function calculaPrazoMutavel(DateTime $dataBase): DateTime {
    return $dataBase->modify('+30 days'); // Altera o original!
}

Encadeamento de métodos com imutabilidade:

$data = (new DateTimeImmutable('2024-01-15'))
    ->modify('+10 days')
    ->modify('next monday')
    ->setTime(9, 0, 0);

echo $data->format('Y-m-d H:i:s'); // 2024-01-29 09:00:00

Conversão entre os tipos:

$mutavel = new DateTime('2024-01-01');
$imutavel = DateTimeImmutable::createFromMutable($mutavel);

$voltaMutavel = DateTime::createFromImmutable($imutavel);

7. Tratamento de erros e validação de datas

Capturando exceções

try {
    $data = new DateTime('data-invalida');
} catch (Exception $e) {
    echo 'Erro ao criar data: ' . $e->getMessage();
}

// Com createFromFormat, verifique o retorno
$data = DateTime::createFromFormat('Y-m-d', '31/02/2024');
if (!$data || $data->getLastErrors()['warning_count'] > 0) {
    echo 'Data inválida ou warnings gerados';
    print_r(DateTime::getLastErrors());
}

Validando strings de data

function validarData(string $data, string $formato = 'Y-m-d'): bool {
    $d = DateTime::createFromFormat($formato, $data);
    return $d && $d->format($formato) === $data;
}

var_dump(validarData('2024-02-29')); // true (ano bissexto)
var_dump(validarData('2023-02-29')); // false
var_dump(validarData('31/12/2024', 'd/m/Y')); // true

A função checkdate() ainda é útil para validação simples:

var_dump(checkdate(2, 29, 2024)); // true
var_dump(checkdate(2, 29, 2023)); // false

Erros comuns a evitar

  • Overflow de datas: modify('+1 month') em 31/01 resulta em 02/03. Use modify('last day of next month') para evitar.
  • Timezone inválido: Sempre verifique com in_array($tz, DateTimeZone::listIdentifiers()).
  • Formato ambíguo: Prefira createFromFormat() a strings soltas.
  • Objetos mutáveis compartilhados: Em funções que recebem DateTime, considere clonar ou usar DateTimeImmutable.
// Solução para overflow de meses
function adicionarMeses(DateTime $data, int $meses): DateTime {
    $clone = clone $data;
    $clone->modify("+{$meses} months");
    return $clone;
}

Dominar DateTime e DateTimeImmutable é essencial para qualquer desenvolvedor PHP que trabalhe com calendários, prazos, agendamentos ou qualquer sistema que dependa de manipulação precisa de datas. A escolha entre mutável e imutável depende do contexto: para objetos que serão compartilhados ou reutilizados, prefira DateTimeImmutable; para operações simples e controladas, DateTime ainda é perfeitamente adequado.

Referências