Cargando…

Esto está tardando más de lo esperado.

Volver al centro de ayuda

Zonas horarias y fechas

Cada fecha se guarda en UTC y se muestra en la zona horaria de la persona, sin código de tu parte.

La regla es una línea: guarda en UTC, muestra en la zona horaria de la persona. El kit la aplica por ti en el modelo, así que una fecha leída de un modelo ya está en la zona horaria de la cuenta con sesión iniciada y una fecha escrita en uno se convierte de vuelta a UTC. Esta guía cubre el trait que lo hace, los dos ayudantes para los casos a los que no llega, cómo se encuentra la zona horaria de una cuenta nueva, y cómo escribir un campo de fecha.

De dónde sale la zona horaria

config/app.php conserva 'timezone' => 'UTC'. Ésa es la zona horaria de la base de datos y de todo lo que se muestra a alguien sin cuenta. Déjala así.

Cada cuenta tiene una columna timezone en users, un identificador de PHP como America/Mexico_City. Todo lo de abajo la lee como auth()->user()?->timezone ?? config('app.timezone'): la zona horaria de la cuenta con sesión iniciada cuando la hay, UTC en otro caso. Un visitante sin sesión, un trabajo en cola y un comando de consola ven, por tanto, UTC.

El trait DatesToUser

App\Traits\DatesToUser sobrescribe getAttribute() y setAttribute() en un modelo:

  • Leer un atributo con cast datetime o timestamp, o uno de los timestamps propios del modelo, created_at y updated_at, devuelve la instancia de Carbon convertida a la zona horaria de la persona.
  • Escribir un valor no nulo en un atributo así lo interpreta en la zona horaria de la persona y lo guarda en UTC.
$post->published_at;                          // Carbon en la zona horaria de la persona
$post->published_at = '2026-06-01 10:00:00';  // leída como su hora local, guardada en UTC
$post->published_at->format('d M Y H:i');     // sin conversión que escribir

Sólo los casts datetime y timestamp se convierten. Una columna con cast date o sin cast vuelve exactamente como se guardó.

Qué modelos lo tienen

App\Models\Model usa el trait, y todos los modelos del kit lo extienden: avisos, categorías, notificaciones, referidos, roles, permisos, sesiones, estadísticas, eventos de seguimiento y de webhook. App\Models\User extiende el Authenticatable de Laravel, así que usa el trait directamente. Los modelos de cobro del paquete billing-core traen su propia variante, DatesToBillingOwner, que convierte a la zona horaria de la cuenta dueña de la suscripción y no a la de quien tiene la sesión iniciada.

Agregarlo a tus modelos

Extiende el modelo base y declara el cast:

namespace App\Models;

class Appointment extends Model
{
    protected $guarded = [];
    protected $casts = [
        'starts_at' => 'datetime',
        'ends_at' => 'datetime',
    ];
}

Un modelo que no puede extender App\Models\Model agrega use App\Traits\DatesToUser; a su clase, como hace User. De cualquier forma, una columna de fecha sin cast datetime o timestamp no se convierte.

El macro de Carbon toUserTimezone()

Para una instancia de Carbon que no salió de un atributo de modelo, como now() o una fecha interpretada desde una petición:

$date->toUserTimezone()->format('d M Y H:i');

Devuelve una copia en la zona horaria de la persona, UTC para un visitante, y deja intacto el original. Está definido en App\Providers\AppServiceProvider.

La directiva de Blade @userDate

<span title="@userDate($notification->created_at)">…</span>

Recibe una expresión que evalúa a una instancia de Carbon y la imprime en la zona horaria de la persona con el formato fijo Y-m-d H:i:s. No tiene opción de formato: para cualquier otro, llama ->toUserTimezone()->format(...) en un {{ }} normal. Pasarle un atributo que el trait ya convirtió es inofensivo; la zona horaria se fija, no se suma.

Cómo recibe su zona horaria una cuenta nueva

No hay JavaScript involucrado y el formulario de registro no tiene campo de zona horaria. Cuando se crea la cuenta, App\Observers\UserObserver le pregunta a App\Services\CountryDetectionService::detectTimezone(), que intenta, en orden:

  1. La zona horaria de la cuenta con sesión iniciada, cuando la hay.
  2. Una zona horaria ya encontrada para este visitante, guardada en la sesión bajo current_timezone.
  3. La IP de la petición, a través de https://ipapi.co/{ip}/timezone/ con un tiempo de espera de dos segundos. Una IP privada, que es toda petición en desarrollo local, se salta este paso.
  4. La zona horaria del país del visitante, a través de la API de Weblabor World, cuando WEBLABOR_WORLD_TOKEN está puesto. El país sale de la detección descrita en Detecta el país del visitante.

Un resultado se revisa contra la lista de identificadores de PHP antes de guardarse. Cuando nada responde, la columna conserva su valor por omisión en la base de datos, que es UTC. Una migración del kit revisita las cuentas que quedaron en UTC y tienen país conocido y llena su zona horaria desde la API de World cuando el token está puesto.

Dónde la cambia la persona

En /account, en la tarjeta "Idioma y hora", desde un select que lista cada identificador de zona horaria de PHP. Tanto el idioma como la zona horaria son obligatorios al guardar esa tarjeta.

Escribir un campo de fecha

Usa el componente <x-date-input>, nunca un <input type="date"> crudo. Su valor fluye por wire:model al atributo del modelo, donde el trait lo convierte de la zona horaria de la persona a UTC.

<x-date-input type="date" wire:model="from" :label="__('From')" />

Con type="date" dibuja el input sencillo del proyecto para una fecha sin hora. Sin type, dibuja un selector de fecha y hora cuyos valores por omisión se cambian con atributos: without-time para quitar el reloj y guardar YYYY-MM-DD, time-format (12 por omisión, 24 para reloj de 24 horas), interval entre minutos (10), min y max, clearable (true), disable-past-dates, start-of-week (domingo), y parse-format y display-format (YYYY-MM-DD HH:mm:ss). Los atributos propios del selector timezone, user-timezone y without-timezone se dejan en sus valores por omisión, que es without-timezone activo, para que el valor llegue al modelo sin tocar y el modelo haga la conversión. Los demás componentes de formulario están en Marca e interfaz.

En el panel de administración, un campo Inputs\DateTime::make('Published At') en un recurso lee y escribe el atributo del modelo, así que recibe la misma conversión sin código extra.