Carbon no Laravel: gerir datas, horas e fusos horários

Carbon no Laravel: gerir datas, horas e fusos horários
Resposta rápida

O Laravel converte automaticamente as colunas de data dos teus modelos em objetos Carbon. Retém três coisas: guarda tudo em UTC e converte apenas na apresentação com ->tz(), declara as tuas próprias colunas em casts(), e não esqueças que no Carbon 3 os métodos diffIn* devolvem um decimal com sinal e já não um inteiro absoluto.

Quase todas as aplicações Laravel manipulam datas: uma data de publicação, o fim de um período experimental, um prazo de entrega, o intervalo entre dois eventos. O Laravel entrega esse trabalho ao Carbon, uma extensão da classe DateTime do PHP, e converte automaticamente as colunas de data dos teus modelos em instâncias de Carbon.

Artigo do percurso Desenvolvimento web. Este guia cobre o que vais realmente fazer: criar uma data, convertê-la para o fuso certo, calcular um intervalo, mostrá-la em francês, manipulá-la no Eloquent e congelá-la nos testes. Todos os resultados apresentados a seguir foram produzidos em Laravel 13.30.1 e Carbon 3.13.2.

O que mudou com o Carbon 3

O Laravel 11, 12 e 13 trazem o Carbon 3. Se estás a reler código escrito para o Laravel 8 ou 9, há duas mudanças que te vão surpreender.

1. Os métodos diffIn* devolvem um número decimal, já não um inteiro. No Carbon 2, diffInDays() devolvia 113. No Carbon 3, a mesma expressão devolve o valor exato:

php
$a = Carbon::parse('2026-09-02 14:30');
$b = Carbon::parse('2026-12-25 08:00');

$a->diffInDays($b);   // 113.72916666666667  (float)
(int) $a->diffInDays($b);   // 113

2. O resultado tem sinal. No Carbon 2, a diferença era absoluta por predefinição. No Carbon 3 é negativa quando a data passada como argumento é a anterior:

php
$a->diffInDays($b);         //  113.72916666666667
$b->diffInDays($a);         // -113.72916666666667
$b->diffInDays($a, true);   //  113.72916666666667   (absolute: true)

Na prática, um if ($date->diffInDays(now()) > 30) escrito para o Carbon 2 pode deixar de disparar depois de uma atualização, porque o valor passou a ser negativo. É a armadilha mais cara da migração.

Já agora, floatDiffInDays() e as suas variantes desapareceram: duplicavam o novo comportamento.

Criar uma data

Os dois helpers globais do Laravel cobrem a maioria das necessidades:

php
now();     // 2026-09-02 14:30:00  — o instante atual
today();   // 2026-09-02 00:00:00  — hoje à meia-noite

Para tudo o resto, passa pela facade. Repara no namespace: o Laravel expõe a sua própria subclasse, Illuminate\Support\Carbon, que acrescenta alguns métodos a Carbon\Carbon. É essa que deves importar numa aplicação Laravel.

app/Services/Planning.php
<?php

use Illuminate\Support\Carbon;

Carbon::now();
Carbon::today();
Carbon::tomorrow('Europe/Paris');
Carbon::yesterday();

Carbon::create(2026, 2, 1, 10, 0, 0, 'Europe/Paris');   // 2026-02-01 10:00:00
Carbon::createFromDate(2026, 2, 1);                     // hora = agora
Carbon::createMidnightDate(2026, 2, 1);                 // hora = 00:00:00
Carbon::createFromTimeString('10:15:00');               // data = hoje
Carbon::createFromFormat('d/m/Y', '01/02/2026');
Cuidado com createFromDate()

createFromDate(2026, 2, 1) não põe a hora a zero: conserva a hora atual. Testada a 2 de setembro de 2026 às 14:30, a chamada devolve 2026-02-01 14:30:00. Usa createMidnightDate() se quiseres a meia-noite.

Carbon::parse() aceita praticamente tudo, incluindo expressões em inglês:

php
Carbon::parse('2026-09-02T14:30:00+02:00');
Carbon::parse('last day of February 2024');   // 2024-02-29 00:00:00

Estas expressões relativas são apelativas, mas frágeis: last day of next month não devolve o mesmo consoante o dia em que o código é executado. Reserva o parse() para as cadeias que vêm de uma fonte máquina (ISO 8601, timestamp) e constrói as restantes datas de forma explícita.

Fusos horários: o único modelo que aguenta

A regra cabe numa frase: guarda tudo em UTC e converte apenas na apresentação. O Laravel segue-a por predefinição, com o config/app.php a fixar 'timezone' => 'UTC'. Não mexas neste valor: é ele que determina o que é escrito na base de dados, e uma base em hora local torna-se ingerível à primeira mudança da hora.

A conversão faz-se sobre a instância, no momento de mostrar:

php
$moment = Carbon::create(2026, 7, 1, 12, 0, 0, 'UTC');

$moment->copy()->tz('Europe/Paris');       // 2026-07-01 14:00:00
$moment->copy()->setTimezone('Asia/Tokyo'); // 2026-07-01 21:00:00

Estes três objetos descrevem o mesmo instante. É verificável: o timestamp Unix é idêntico. Mudar o fuso não desloca o momento, muda a forma de o ler.

Convém, no entanto, distinguir duas escritas que se parecem:

php
Carbon::tomorrow('Europe/Paris');        // meia-noite em Paris
Carbon::tomorrow()->tz('Europe/Paris');  // meia-noite UTC, lida na hora de Paris (02:00)

A primeira cria uma data no fuso. A segunda cria uma data em UTC e depois converte-a. A lista de fusos aceites é a do PHP, 419 entradas no PHP 8.4.25:

php
timezone_identifiers_list();   // ['Africa/Abidjan', 'Africa/Accra', …]

Para guardar o fuso de um utilizador, acrescenta uma coluna timezone à tabela users e alimenta-a a partir de uma lista pendente construída com estes identificadores. Um pacote como jamesmills/laravel-timezone automatiza a deteção na autenticação, mas na maioria dos casos basta uma coluna e um formulário.

Somar e subtrair

Cada unidade tem um método no singular e outro no plural, tanto para somar como para subtrair:

php
now()->addDay();
now()->addDays(30);
now()->subDay();
now()->subDays(30);

now()->addWeeks(2);
now()->addMonths(3);
now()->addYears(5);
now()->addHours(6);
now()->addMinutes(90);
now()->subWeekdays(3);   // ignora sábado e domingo

Há dois comportamentos que vale a pena conhecer antes de os descobrires em produção.

O Carbon é mutável. addMonth() modifica o objeto sobre o qual o chamas, não devolve uma cópia:

php
$date = Carbon::create(2026, 1, 31);
$date->addMonth();
echo $date;   // 2026-03-03  —  $date mudou

$date = Carbon::create(2026, 1, 31);
echo $date->copy()->addMonth();   // 2026-03-03
echo $date;                       // 2026-01-31  —  $date está intacta

Os meses transbordam. 31 de janeiro mais um mês dá 3 de março, porque o 31 de fevereiro não existe e o Carbon arrasta o excedente. Se o que queres é o fim do mês, pede-o:

php
Carbon::create(2026, 1, 31)->copy()->addMonth();            // 2026-03-03
Carbon::create(2026, 1, 31)->copy()->addMonthNoOverflow();  // 2026-02-28

Evitar as mutações com o CarbonImmutable

Em vez de semear copy() por todo o lado, podes tornar imutáveis todas as datas da aplicação. Cada operação devolve então uma nova instância e a original nunca se mexe:

app/Providers/AppServiceProvider.php
use Carbon\CarbonImmutable;
use Illuminate\Support\Facades\Date;

public function boot(): void
{
    Date::use(CarbonImmutable::class);
}

Depois desta chamada, now() devolve uma instância de Carbon\CarbonImmutable e as datas hidratadas pelo Eloquent também. É uma mudança de comportamento global: decide-a no arranque do projeto, não numa aplicação já escrita com chamadas a addDay() que contam com a mutação.

Calcular o intervalo entre duas datas

Com os valores com sinal e em vírgula flutuante do Carbon 3, diz sempre exatamente o que queres:

php
$debut = Carbon::parse('2026-09-02 14:30');
$fin   = Carbon::parse('2026-12-25 08:00');

$debut->diffInDays($fin);              // 113.72916666666667
$debut->diffInDays($fin, true);        // valor absoluto
(int) $debut->diffInDays($fin);        // 113, dias inteiros
round($debut->diffInHours($fin), 2);   // 2729.5
$debut->diffInWeekdays($fin);          // 82, sem fins de semana
$debut->diffInMonths($fin);            // 3.733198924731183

Para uma apresentação legível, o diffForHumans() trata da formulação, inclusive em francês:

php
$fin->diffForHumans($debut);                 // '3 months after'
$fin->locale('fr')->diffForHumans($debut);   // '3 mois après'
$fin->locale('fr')->diffForHumans();         // 'dans 3 mois' (em relação a agora)

Mostrar uma data em francês

O format() usa os códigos do PHP e não traduz nada: os nomes dos dias e dos meses continuam em inglês. Para francês, precisas de translatedFormat() ou de isoFormat().

php
$d = Carbon::parse('2026-09-02 14:30');

$d->toDateTimeString();                      // '2026-09-02 14:30:00'
$d->toDateString();                          // '2026-09-02'
$d->toIso8601String();                       // '2026-09-02T14:30:00+00:00'
$d->format('d/m/Y H:i');                     // '02/09/2026 14:30'
$d->locale('fr')->translatedFormat('l j F Y'); // 'mercredi 2 septembre 2026'
$d->locale('fr')->isoFormat('LLLL');         // 'mercredi 2 septembre 2026 14:30'

O translatedFormat() mantém os códigos do PHP e traduz os rótulos. O isoFormat() usa códigos do tipo LLLL, LL ou dddd, que se adaptam às convenções de cada língua. Para não escreveres locale('fr') por todo o lado, define a locale da aplicação uma única vez:

.env
APP_LOCALE=fr
APP_FAKER_LOCALE=fr_FR

O Carbon nos modelos Eloquent

created_at e updated_at são convertidas automaticamente, desde que as colunas existam mesmo na base de dados. Chegam já como instâncias de Illuminate\Support\Carbon, não como cadeias.

php
$post = Post::first();

$post->created_at->locale('fr')->isoFormat('LL');   // '2 septembre 2026'
$post->created_at->diffForHumans();
$post->created_at->isToday();

Numa vista Blade, estas chamadas escrevem-se da mesma forma entre chavetas.

Para as tuas próprias colunas, acrescentadas por uma migração, declara-as no método casts(). Atenção: a antiga propriedade protected $dates = [...] foi retirada no Laravel 10 e já não tem qualquer efeito. Código que ainda dependa dela deixa-te cadeias de caracteres onde esperavas objetos.

app/Models/Post.php
protected function casts(): array
{
    return [
        'published_at' => 'datetime',
        'expires_at'   => 'datetime:Y-m-d',
        'starts_at'    => 'immutable_datetime',
    ];
}

O formato indicado depois de datetime: não altera o armazenamento, apenas a serialização em JSON. As comparações de datas escrevem-se então com naturalidade nas consultas Eloquent:

php
Post::where('published_at', '<=', now())->get();
Post::whereDate('published_at', today())->get();
Post::whereBetween('published_at', [now()->subWeek(), now()])->get();

Congelar o tempo nos testes

Um teste que dependa do relógio real acaba sempre por falhar num 29 de fevereiro ou depois da meia-noite. O Carbon sabe mentir sobre a data atual, e o Laravel expõe helpers para isso:

tests/Feature/TrialTest.php
$this->travelTo(Carbon::parse('2026-09-02 14:30'));

expect(now()->toDateTimeString())->toBe('2026-09-02 14:30:00');

$this->travel(5)->days();   // avança o relógio 5 dias
$this->travelBack();        // volta à hora real

Fora dos testes do Laravel, o equivalente em cru é Carbon::setTestNow(), que se anula com uma chamada sem argumentos. Foi este mecanismo que serviu para produzir os valores deste guia, todos calculados a partir de 2 de setembro de 2026 às 14:30 UTC.

php
Carbon::setTestNow('2026-09-02 14:30:00');
// …
Carbon::setTestNow();   // liberta o relógio

Erros frequentes

diffInDays() devolve um decimal com sinal Desde o Carbon 3, a diferença é negativa se o argumento for anterior, e traz casas decimais. Uma condição escrita para o Carbon 2 pode deixar de disparar: passa absolute: true ou converte para (int).
O Carbon é mutável $date->addMonth() modifica $date. Usa copy() antes da operação, ou passa a aplicação para CarbonImmutable através de Date::use().
Os meses transbordam 31 de janeiro + 1 mês dá 3 de março, não 28 de fevereiro. Usa addMonthNoOverflow() para ficares dentro do mês.
A propriedade $dates já não faz nada Retirada no Laravel 10. Declara as tuas colunas de data no método casts(), caso contrário estás a manipular cadeias.
createFromDate() não põe a hora a zero Conserva a hora atual. createMidnightDate() devolve mesmo 00:00:00.
format() não traduz Os nomes dos dias e dos meses continuam em inglês. Usa translatedFormat() ou isoFormat().

CarbonLaravelPHP

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.