Tema
Fechas y horas
Reservas, vencimientos de suscripciones, "publicado hace 3 horas", facturas del mes... Las fechas están en todas partes y son una fuente clásica de errores. Aquí aprenderás a manejarlas con la API moderna de PHP (DateTimeImmutable) y a evitar los problemas de zonas horarias.
Concepto
Internamente, una fecha es un instante en el tiempo. PHP puede representarlo de dos formas:
- Timestamp Unix: un número entero con los segundos transcurridos desde el 1 de enero de 1970 a las 00:00 UTC. Ejemplo:
1760745600. - Objetos
DateTime/DateTimeImmutable: objetos con métodos para sumar días, comparar, formatear y cambiar de zona horaria.
La zona horaria es como el "idioma" en que expresas la hora. El mismo instante es las 12:00 en Ciudad de México y las 20:00 en Madrid. Si no la configuras, PHP usa la de php.ini (o UTC) y tus horas saldrán desfasadas.
Configurar la zona horaria
php
<?php
// Al inicio de tu aplicación (por ejemplo en config.php)
date_default_timezone_set('America/Mexico_City');
// Otras comunes: 'America/Bogota', 'America/Lima', 'America/Argentina/Buenos_Aires',
// 'Europe/Madrid', 'America/Santiago', 'UTC'
echo date_default_timezone_get(); // America/Mexico_CityO en php.ini, para todo el servidor:
ini
date.timezone = "America/Mexico_City"Funciones clásicas: date() y time()
php
<?php
echo time(); // 1760745600 (timestamp actual)
echo date('Y-m-d'); // 2026-10-18
echo date('d/m/Y H:i:s'); // 18/10/2026 14:35:09
echo date('Y-m-d', 0); // 1970-01-01 (timestamp 0)
// strtotime convierte texto en timestamp
$manana = strtotime('+1 day');
echo date('Y-m-d', $manana);
$proximoLunes = strtotime('next monday');
echo date('l d/m/Y', $proximoLunes); // Monday 19/10/2026Caracteres de formato más usados
| Carácter | Significado | Ejemplo |
|---|---|---|
Y | Año 4 dígitos | 2026 |
y | Año 2 dígitos | 26 |
m | Mes con cero | 03 |
n | Mes sin cero | 3 |
d | Día con cero | 07 |
j | Día sin cero | 7 |
H | Hora 24h | 14 |
h | Hora 12h | 02 |
i | Minutos | 05 |
s | Segundos | 09 |
A | AM/PM | PM |
N | Día semana (1=lunes) | 1 |
t | Días del mes | 31 |
c | ISO 8601 | 2026-10-18T14:35:09-06:00 |
La forma moderna: DateTimeImmutable
php
<?php
declare(strict_types=1);
date_default_timezone_set('America/Mexico_City');
// Fecha y hora actual
$ahora = new DateTimeImmutable();
echo $ahora->format('Y-m-d H:i'); // 2026-10-18 14:35
// Desde un texto
$cumple = new DateTimeImmutable('2000-05-21');
// Desde un formato específico (útil para fechas en formato latino)
$fecha = DateTimeImmutable::createFromFormat('d/m/Y', '25/12/2026');
echo $fecha->format('Y-m-d'); // 2026-12-25
// Sumar y restar con modify()
$vence = $ahora->modify('+30 days');
$ayer = $ahora->modify('-1 day');
$finDeMes = $ahora->modify('last day of this month');
// Sumar con intervalos (formato ISO 8601: P = periodo, T = tiempo)
$enDosHoras = $ahora->add(new DateInterval('PT2H')); // 2 horas
$enUnAnio = $ahora->add(new DateInterval('P1Y')); // 1 año
$en1m15d = $ahora->add(new DateInterval('P1M15D')); // 1 mes y 15 días¿Por qué Immutable y no DateTime?
php
<?php
// Con DateTime (mutable), modify() CAMBIA el objeto original
$inicio = new DateTime('2026-01-01');
$fin = $inicio->modify('+7 days');
echo $inicio->format('Y-m-d'); // 2026-01-08 ¡Se modificó el inicio!
// Con DateTimeImmutable, modify() devuelve un objeto NUEVO
$inicio = new DateTimeImmutable('2026-01-01');
$fin = $inicio->modify('+7 days');
echo $inicio->format('Y-m-d'); // 2026-01-01 intacto
echo $fin->format('Y-m-d'); // 2026-01-08Buena práctica
Usa siempre DateTimeImmutable. Evita errores difíciles de encontrar donde una fecha cambia "sola" porque otra parte del código la modificó.
Comparar fechas y calcular diferencias
php
<?php
$hoy = new DateTimeImmutable('today');
$evento = new DateTimeImmutable('2026-12-24');
// Los objetos se comparan directamente con < > ==
if ($evento > $hoy) {
echo 'El evento aún no ocurre';
}
// diff() devuelve un DateInterval
$diferencia = $hoy->diff($evento);
echo $diferencia->days; // días totales: 67
echo $diferencia->m; // meses (parte): 2
echo $diferencia->d; // días (parte): 6
echo $diferencia->invert; // 1 si $evento es anterior a $hoy
// Formatear un intervalo
echo $diferencia->format('%m meses y %d días'); // 2 meses y 6 díasCalcular la edad
php
<?php
function calcularEdad(string $fechaNacimiento): int
{
$nacimiento = new DateTimeImmutable($fechaNacimiento);
$hoy = new DateTimeImmutable('today');
return $nacimiento->diff($hoy)->y; // años completos
}
echo calcularEdad('2000-05-21'); // 26Fechas en español
date() y format() siempre muestran los nombres en inglés. Para español usa la extensión intl (viene en XAMPP/Laragon; actívala en php.ini con extension=intl):
php
<?php
$fecha = new DateTimeImmutable('2026-10-18');
$formateador = new IntlDateFormatter(
'es_MX', // idioma y país
IntlDateFormatter::FULL, // estilo de fecha
IntlDateFormatter::NONE, // sin hora
'America/Mexico_City'
);
echo $formateador->format($fecha); // domingo, 18 de octubre de 2026
// Con un patrón personalizado (sintaxis ICU, distinta de date())
$corto = new IntlDateFormatter('es_MX', pattern: "d 'de' MMMM, yyyy");
echo $corto->format($fecha); // 18 de octubre, 2026Si no tienes intl, una alternativa sencilla:
php
<?php
function fechaEnEspanol(DateTimeInterface $f): string
{
$dias = ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'];
$meses = ['enero', 'febrero', 'marzo', 'abril', 'mayo', 'junio', 'julio',
'agosto', 'septiembre', 'octubre', 'noviembre', 'diciembre'];
return sprintf(
'%s %d de %s de %d',
$dias[(int) $f->format('w')], // w: 0 = domingo
(int) $f->format('j'),
$meses[(int) $f->format('n') - 1], // n: 1 = enero
(int) $f->format('Y')
);
}
echo fechaEnEspanol(new DateTimeImmutable('2026-10-18')); // domingo 18 de octubre de 2026Caso real: "hace X tiempo"
php
<?php
declare(strict_types=1);
/**
* Convierte una fecha en un texto relativo: "hace 5 minutos", "hace 2 días"...
*/
function haceCuanto(string $fecha): string
{
$entonces = new DateTimeImmutable($fecha);
$ahora = new DateTimeImmutable();
$segundos = $ahora->getTimestamp() - $entonces->getTimestamp();
return match (true) {
$segundos < 60 => 'hace un momento',
$segundos < 3600 => 'hace ' . intdiv($segundos, 60) . ' min',
$segundos < 86400 => 'hace ' . intdiv($segundos, 3600) . ' h',
$segundos < 604800 => 'hace ' . intdiv($segundos, 86400) . ' días',
default => 'el ' . $entonces->format('d/m/Y'),
};
}
echo haceCuanto('-3 hours'); // hace 3 hFechas y bases de datos
MySQL guarda fechas en formato Y-m-d H:i:s. Convierte siempre a ese formato antes de guardar:
php
<?php
// El usuario escribe 25/12/2026 en un input de texto
$entrada = $_POST['fecha'] ?? '';
$fecha = DateTimeImmutable::createFromFormat('!d/m/Y', $entrada);
// El "!" pone la hora en 00:00:00 en vez de la hora actual
// createFromFormat devuelve false si el formato no coincide
if ($fecha === false || $fecha->format('d/m/Y') !== $entrada) {
// La segunda comprobación descarta fechas como 31/02/2026
exit('Fecha inválida');
}
$paraMySQL = $fecha->format('Y-m-d'); // 2026-12-25Un <input type="date"> en HTML ya envía el formato Y-m-d, lo que facilita todo:
php
<?php
$fecha = DateTimeImmutable::createFromFormat('!Y-m-d', $_POST['fecha'] ?? '');Buena práctica
En aplicaciones con usuarios en varios países, guarda las fechas en UTC en la base de datos y conviértelas a la zona horaria del usuario sólo al mostrarlas:
php
$utc = new DateTimeImmutable('2026-10-18 20:00:00', new DateTimeZone('UTC'));
$local = $utc->setTimezone(new DateTimeZone('America/Bogota'));
echo $local->format('H:i'); // 15:00Error común
Confundir m (mes) con i (minutos). date('H:m') muestra la hora y el mes, no los minutos. Lo correcto es date('H:i').
Error común
Validar una fecha sólo con strtotime(). Acepta cosas como "31 February" y las convierte silenciosamente al 3 de marzo. Usa createFromFormat + comparación, o checkdate($mes, $dia, $anio).
Ejercicios
- Escribe una función
diasHasta(string $fecha): intque devuelva cuántos días faltan para una fecha (negativo si ya pasó). - Genera un calendario simple: lista todos los días del mes actual con su nombre en español, marcando los fines de semana.
- Una suscripción empieza el 31 de enero de 2026 y se renueva cada mes. Calcula las 3 próximas fechas de renovación y descubre qué pasa con febrero. ¿Cómo lo arreglarías?
Ver solución
php
<?php
declare(strict_types=1);
date_default_timezone_set('America/Mexico_City');
// 1
function diasHasta(string $fecha): int
{
$hoy = new DateTimeImmutable('today');
$objetivo = new DateTimeImmutable($fecha);
$diff = $hoy->diff($objetivo);
return $diff->invert ? -$diff->days : $diff->days;
}
echo diasHasta('2026-12-31'), PHP_EOL;
// 2
$dias = ['domingo', 'lunes', 'martes', 'miércoles', 'jueves', 'viernes', 'sábado'];
$inicio = new DateTimeImmutable('first day of this month');
$fin = $inicio->modify('first day of next month');
// DatePeriod recorre fechas de un intervalo
$periodo = new DatePeriod($inicio, new DateInterval('P1D'), $fin);
foreach ($periodo as $dia) {
$nombre = $dias[(int) $dia->format('w')];
$finde = in_array((int) $dia->format('w'), [0, 6], true) ? ' (fin de semana)' : '';
echo $dia->format('d') . " $nombre$finde" . PHP_EOL;
}
// 3: el problema
$inicio = new DateTimeImmutable('2026-01-31');
echo $inicio->modify('+1 month')->format('Y-m-d'), PHP_EOL; // 2026-03-03 (¡se salta febrero!)
// Solución: calcular siempre desde la fecha original y limitar al último día del mes
function renovacion(DateTimeImmutable $inicio, int $meses): DateTimeImmutable
{
$primerDia = $inicio->modify('first day of this month')->modify("+$meses months");
$dia = min((int) $inicio->format('j'), (int) $primerDia->format('t'));
return $primerDia->setDate((int) $primerDia->format('Y'), (int) $primerDia->format('n'), $dia);
}
for ($i = 1; $i <= 3; $i++) {
echo renovacion($inicio, $i)->format('Y-m-d'), PHP_EOL;
}
// 2026-02-28, 2026-03-31, 2026-04-30Resumen
- Configura la zona horaria con
date_default_timezone_set()o en php.ini. date()ytime()sirven para casos rápidos; para todo lo demás,DateTimeImmutable.modify(),add()yDateIntervalsuman o restan tiempo;diff()calcula diferencias.createFromFormat()convierte texto con formato conocido y permite validar.- Para nombres en español usa
IntlDateFormattero un arreglo propio. - Guarda en formato
Y-m-d H:i:sen MySQL, idealmente en UTC.