Fechas y horas en PHP
En esta lección
Casi cualquier web trabaja con fechas: el día de una reserva, la caducidad de una cuota, la antigüedad de un comentario, el año del pie de página. PHP tiene dos formas de manejarlas: las funciones clásicas (time(), date(), mktime()), sencillas y suficientes para mostrar la fecha de hoy, y las clases DateTime, más completas para calcular, comparar y validar. En esta lección aprenderás las dos, empezando por algo que conviene dejar resuelto antes de nada: la zona horaria.
Primero, la zona horaria
Una misma instante es la una del mediodía en Madrid y las doce en Londres. PHP necesita saber en qué zona horaria trabajas para darte la hora correcta. Si no se lo dices, usa la que tenga configurada el servidor (muchas veces UTC, la hora universal), y tus horas saldrán desplazadas una o dos horas.
Tienes dos formas de fijarla:
- En el archivo
php.ini, con la directivadate.timezone = Europe/Madrid. Afecta a todos los scripts del servidor. - En tu código, al principio, con
date_default_timezone_set():
date_default_timezone_set('Europe/Madrid');
echo date_default_timezone_get(), "\n";
// Europe/Madrid
Los nombres de zona siguen el formato Continente/Ciudad: Europe/Madrid, Atlantic/Canary, America/Mexico_City. Ponlo en tu archivo de configuración común (lo viste en include y require) y te olvidas. Todos los ejemplos de esta lección usan Europe/Madrid.
El timestamp: una fecha como un número
Internamente, PHP (y casi todos los sistemas) guarda los instantes como un timestamp Unix: el número de segundos transcurridos desde el 1 de enero de 1970 a las 00:00 en UTC. Es un simple entero, así que es fácil de guardar y comparar.
echo time(), "\n";
// 1791289325 (cambia cada segundo)
time() devuelve el timestamp del momento actual. Por sí solo no dice mucho a una persona; para convertirlo en algo legible está date().
date(): dar formato a una fecha
date($formato, $timestamp) devuelve una cadena con la fecha según el formato que le indiques. Si no le pasas el timestamp, usa el momento actual:
echo date('d/m/Y'), "\n";
echo date('H:i'), "\n";
echo date('Y'), "\n";
// 06/10/2026 (el día de hoy)
// 18:42 (la hora actual)
// 2026
El formato es una cadena donde cada letra se sustituye por una parte de la fecha. Las más usadas:
| Letra | Significado | Ejemplo |
|---|---|---|
d | Día del mes con dos cifras | 05 |
j | Día del mes sin cero inicial | 5 |
m | Mes con dos cifras | 03 |
n | Mes sin cero inicial | 3 |
Y | Año con cuatro cifras | 2026 |
y | Año con dos cifras | 26 |
H | Hora de 00 a 23 | 09 |
G | Hora de 0 a 23, sin cero | 9 |
i | Minutos | 07 |
s | Segundos | 45 |
N | Día de la semana, de 1 (lunes) a 7 (domingo) | 4 |
w | Día de la semana, de 0 (domingo) a 6 (sábado) | 4 |
t | Días que tiene el mes | 31 |
L | 1 si el año es bisiesto, 0 si no | 0 |
D / l | Día de la semana en inglés (corto / largo) | Thu / Thursday |
M / F | Mes en inglés (corto / largo) | Mar / March |
Cualquier otro carácter, como /, -, : o un espacio, se copia tal cual. Si quieres que una letra aparezca literalmente, escápala con \: date('d/m/Y \a \l\a\s H:i') da algo como 05/03/2026 a las 09:07. Con tantas barras se lee mal; a menudo es más limpio concatenar: date('d/m/Y') . ' a las ' . date('H:i').
Construir una fecha concreta: mktime
mktime($hora, $minuto, $segundo, $mes, $dia, $anio) devuelve el timestamp de una fecha concreta. Fíjate en el orden: el mes va antes que el día, al estilo estadounidense.
date_default_timezone_set('Europe/Madrid');
$inauguracion = mktime(18, 30, 0, 3, 5, 2026);
echo date('d/m/Y H:i', $inauguracion), "\n";
echo date('N', $inauguracion), " = jueves\n";
echo date('t', mktime(0, 0, 0, 2, 1, 2028)), " días tiene febrero de 2028\n";
// 05/03/2026 18:30
// 4 = jueves
// 29 días tiene febrero de 2028
mktime() corrige los valores fuera de rango: el día 32 de enero se convierte en el 1 de febrero. A veces es útil (el día 0 de un mes es el último del anterior), pero también puede esconder errores.
checkdate: ¿existe esta fecha?
checkdate($mes, $dia, $anio) dice si una fecha existe de verdad, teniendo en cuenta los bisiestos:
var_dump(checkdate(2, 29, 2027));
var_dump(checkdate(2, 29, 2028));
var_dump(checkdate(4, 31, 2026));
// bool(false)
// bool(true)
// bool(false)
strtotime: fechas escritas en inglés
strtotime() convierte un texto en un timestamp. Entiende fechas en formato Y-m-d y expresiones en inglés como tomorrow, +1 week o next monday:
date_default_timezone_set('Europe/Madrid');
$base = mktime(12, 0, 0, 3, 5, 2026); // jueves 5 de marzo de 2026
echo date('d/m/Y', strtotime('2026-12-24')), "\n";
echo date('d/m/Y', strtotime('+10 days', $base)), "\n";
echo date('d/m/Y', strtotime('next monday', $base)), "\n";
// 24/12/2026
// 15/03/2026
// 09/03/2026
Si no entiende el texto, devuelve false. Para cualquier cálculo que vaya más allá de esto, pasa a las clases de fecha.
Cuidado:
strtotime('05/03/2026')interpreta la fecha como mes/día/año (el 3 de mayo). Las fechas con barras siempre se leen al estilo estadounidense. Para leer fechas en formato español usacreateFromFormat(), que verás más abajo.
DateTimeImmutable: fechas como objetos
Las funciones anteriores se quedan cortas en cuanto necesitas sumar meses, calcular cuántos días faltan o trabajar con varias zonas horarias. Para eso PHP tiene dos clases casi idénticas:
DateTime: sus métodos modifican el propio objeto.DateTimeImmutable: sus métodos devuelven un objeto nuevo y dejan el original intacto.
Usa DateTimeImmutable por defecto. Con DateTime es muy fácil cambiar una fecha sin querer:
$inicio = new DateTime('2026-03-05');
$fin = $inicio->modify('+1 week');
echo $inicio->format('d/m'), " - ", $fin->format('d/m'), "\n";
$inicio = new DateTimeImmutable('2026-03-05');
$fin = $inicio->modify('+1 week');
echo $inicio->format('d/m'), " - ", $fin->format('d/m'), "\n";
// 12/03 - 12/03
// 05/03 - 12/03
Con DateTime, modify() ha cambiado también $inicio: las dos variables apuntan al mismo objeto. Con DateTimeImmutable cada cálculo produce una fecha nueva. Las clases y objetos las verás más adelante; de momento basta con saber que new crea el objeto y -> llama a sus métodos.
Crear y mostrar
date_default_timezone_set('Europe/Madrid');
$ahora = new DateTimeImmutable();
$examen = new DateTimeImmutable('2026-06-15 09:30');
$enCanarias = new DateTimeImmutable('2026-06-15 09:30', new DateTimeZone('Atlantic/Canary'));
echo $examen->format('d/m/Y H:i'), "\n";
echo $examen->format('Y-m-d H:i:s'), "\n";
echo $enCanarias->format('H:i T'), "\n";
echo $examen->getTimestamp() - $enCanarias->getTimestamp(), " segundos de diferencia\n";
// 15/06/2026 09:30
// 2026-06-15 09:30:00
// 09:30 WEST
// -3600 segundos de diferencia
- Sin argumentos, el objeto representa el momento actual.
- Con una cadena, entiende lo mismo que
strtotime(). El formato más seguro esY-m-d H:i:s. format()usa las mismas letras quedate().Tmuestra la abreviatura de la zona (CEST,WEST…).getTimestamp()devuelve el timestamp; las 9:30 de Madrid ocurren una hora antes que las 9:30 de Canarias.
Sumar y restar: modify, add y sub
Tienes dos formas de mover una fecha. modify() acepta las mismas expresiones en inglés que strtotime(); add() y sub() reciben un DateInterval, un periodo de tiempo escrito con un código que empieza por P (period):
$alta = new DateTimeImmutable('2026-03-05 10:00');
echo $alta->modify('+15 days')->format('d/m/Y'), "\n";
echo $alta->modify('last day of this month')->format('d/m/Y'), "\n";
echo $alta->add(new DateInterval('P1Y'))->format('d/m/Y'), "\n";
echo $alta->add(new DateInterval('P1M15D'))->format('d/m/Y'), "\n";
echo $alta->sub(new DateInterval('PT2H30M'))->format('d/m/Y H:i'), "\n";
// 20/03/2026
// 31/03/2026
// 05/03/2027
// 20/04/2026
// 05/03/2026 07:30
En el código de DateInterval, Y son años, M meses, D días y W semanas. Las horas, minutos y segundos van detrás de una T: PT2H30M son 2 horas y 30 minutos. La T evita la confusión entre M de meses y M de minutos.
El problema del fin de mes
¿Qué es “un mes después del 31 de enero”? PHP suma uno al mes (31 de febrero) y, como no existe, se desborda:
$cobro = new DateTimeImmutable('2026-01-31');
echo $cobro->modify('+1 month')->format('d/m/Y'), "\n";
echo $cobro->modify('last day of next month')->format('d/m/Y'), "\n";
// 03/03/2026
// 28/02/2026
Si calculas vencimientos mensuales, usa last day of next month o guarda el día del mes y ajústalo tú.
Diferencia entre dos fechas
diff() devuelve un DateInterval con la diferencia. Su propiedad days tiene el total de días, y y, m y d el desglose en años, meses y días:
$nacimiento = new DateTimeImmutable('2004-11-20');
$hoy = new DateTimeImmutable('2026-10-06');
$edad = $nacimiento->diff($hoy);
echo "Tiene {$edad->y} años\n";
echo "Ha vivido {$edad->days} días\n";
echo $edad->format('%y años, %m meses y %d días'), "\n";
$finCurso = new DateTimeImmutable('2027-06-18');
echo "Quedan ", $hoy->diff($finCurso)->days, " días para el fin de curso\n";
// Tiene 21 años
// Ha vivido 7990 días
// 21 años, 10 meses y 16 días
// Quedan 255 días para el fin de curso
days siempre es positivo. Si necesitas saber cuál de las dos es anterior, mira la propiedad invert (vale 1 si la segunda fecha es anterior) o, más sencillo, compara las fechas.
Comparar fechas
Los objetos de fecha se comparan directamente con <, >, ==…:
$hoy = new DateTimeImmutable('2026-10-06');
$caducidad = new DateTimeImmutable('2026-09-30');
if ($caducidad < $hoy) {
echo "Cuota caducada\n";
}
var_dump(new DateTimeImmutable('2026-10-06 00:00') == $hoy);
// Cuota caducada
// bool(true)
Ten en cuenta que comparan también la hora. new DateTimeImmutable('today') crea la fecha de hoy a las 00:00, útil para comparar solo días.
Recorrer un periodo: DatePeriod
DatePeriod genera las fechas entre un inicio y un fin, saltando un intervalo cada vez. Perfecto para pintar un calendario o un horario:
$diasSemana = ['', 'lun', 'mar', 'mié', 'jue', 'vie', 'sáb', 'dom'];
$periodo = new DatePeriod(
new DateTimeImmutable('2026-03-02'),
new DateInterval('P1D'),
new DateTimeImmutable('2026-03-07')
);
foreach ($periodo as $dia) {
echo $diasSemana[$dia->format('N')], " ", $dia->format('j'), "\n";
}
// lun 2
// mar 3
// mié 4
// jue 5
// vie 6
La fecha final no se incluye.
Leer y validar fechas: createFromFormat
Cuando una fecha llega de un formulario o de un archivo con un formato concreto, no adivines: dile a PHP qué formato tiene con createFromFormat(). Devuelve el objeto, o false si el texto no encaja:
$fecha = DateTimeImmutable::createFromFormat('d/m/Y', '05/03/2026');
echo $fecha->format('Y-m-d'), "\n";
var_dump(DateTimeImmutable::createFromFormat('d/m/Y', '2026-03-05'));
// 2026-03-05
// bool(false)
Pero cuidado: igual que mktime(), corrige las fechas imposibles en lugar de rechazarlas. El 31 de abril se convierte en el 1 de mayo. Para validar de verdad, comprueba que al volver a formatear la fecha obtienes exactamente el mismo texto:
function fechaValida(string $texto, string $formato = 'Y-m-d'): bool
{
$fecha = DateTimeImmutable::createFromFormat('!' . $formato, $texto);
return $fecha !== false && $fecha->format($formato) === $texto;
}
var_dump(fechaValida('2026-04-30'));
var_dump(fechaValida('2026-04-31'));
var_dump(fechaValida('30/04/2026', 'd/m/Y'));
var_dump(fechaValida('ayer'));
// bool(true)
// bool(false)
// bool(true)
// bool(false)
El ! al principio del formato pone a cero todo lo que no aparece en él (la hora, por ejemplo). Sin él, los campos que faltan se rellenan con la hora actual, y dos fechas “del mismo día” podrían no ser iguales al compararlas.
Esta función completa lo que viste en Validar y sanear. Recuerda que un campo <input type="date"> envía la fecha siempre en formato Y-m-d (2026-03-05), aunque el navegador la muestre como 05/03/2026, y uno <input type="datetime-local">, en formato Y-m-d\TH:i.
Fechas en español
format() y date() solo escriben nombres de días y meses en inglés. La solución más sencilla y que funciona en cualquier servidor es un array con los nombres:
function fechaLarga(DateTimeImmutable $fecha): 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 $dias[$fecha->format('w')] . ', ' . $fecha->format('j') . ' de '
. $meses[$fecha->format('n') - 1] . ' de ' . $fecha->format('Y');
}
echo fechaLarga(new DateTimeImmutable('2026-03-05')), "\n";
// jueves, 5 de marzo de 2026
Si el servidor tiene la extensión intl, la clase IntlDateFormatter lo hace por ti y conoce todos los idiomas:
$formateador = new IntlDateFormatter('es_ES', IntlDateFormatter::FULL, IntlDateFormatter::NONE, 'Europe/Madrid');
echo $formateador->format(new DateTimeImmutable('2026-03-05')), "\n";
// jueves, 5 de marzo de 2026
Nota: en código antiguo verás
setlocale()constrftime()para esto.strftime()está obsoleta desde PHP 8.1; no la uses.
Fechas y bases de datos
MySQL guarda las fechas en columnas DATE (2026-03-05) y DATETIME (2026-03-05 18:30:00). Por eso, para guardar una fecha, usa el formato Y-m-d H:i:s, y para leerla, crea el objeto directamente con la cadena:
$reserva = new DateTimeImmutable('2026-03-05 18:30');
$paraGuardar = $reserva->format('Y-m-d H:i:s');
echo $paraGuardar, "\n";
$leida = new DateTimeImmutable('2026-03-05 18:30:00');
echo $leida->format('d/m/Y \a \l\a\s H:i'), "\n";
// 2026-03-05 18:30:00
// 05/03/2026 a las 18:30
Regla práctica: guarda y calcula en formato Y-m-d; convierte al formato español solo al mostrar. Lo pondrás en práctica en MySQL con PDO.
Ejemplo completo: estado de un bono
Un bono de 10 entradas caduca a los 6 meses de comprarlo. Queremos mostrar su estado:
date_default_timezone_set('Europe/Madrid');
function estadoBono(string $fechaCompra, DateTimeImmutable $hoy): string
{
$compra = new DateTimeImmutable($fechaCompra);
$caducidad = $compra->add(new DateInterval('P6M'));
if ($caducidad < $hoy) {
return "Caducado el " . $caducidad->format('d/m/Y');
}
$quedan = $hoy->diff($caducidad)->days;
$aviso = $quedan <= 15 ? " (¡renuévalo pronto!)" : "";
return "Válido hasta el " . $caducidad->format('d/m/Y') . ", quedan $quedan días" . $aviso;
}
$hoy = new DateTimeImmutable('2026-10-06');
echo estadoBono('2026-03-20', $hoy), "\n";
echo estadoBono('2026-04-15', $hoy), "\n";
echo estadoBono('2026-09-01', $hoy), "\n";
// Caducado el 20/09/2026
// Válido hasta el 15/10/2026, quedan 9 días (¡renuévalo pronto!)
// Válido hasta el 01/03/2027, quedan 146 días
La función recibe la fecha de hoy como parámetro en lugar de crearla dentro. Así puedes probarla con cualquier día; en la web real la llamarías con new DateTimeImmutable('today').
Errores frecuentes
- No fijar la zona horaria: las horas salen desplazadas. Usa
date_default_timezone_set('Europe/Madrid')o configuraphp.ini. - Confundir
mconi:mes el mes; los minutos soni.date('H:m')muestra la hora y el mes. - Pasar fechas españolas a
strtotime():05/03/2026se lee como 3 de mayo. UsacreateFromFormat('d/m/Y', ...). - Usar
DateTimey modificar la fecha original sin querer: usaDateTimeImmutable. - Fiarse de
createFromFormat()para validar: acepta el 31 de abril y lo convierte en 1 de mayo. Compara el resultado formateado con el texto original. - Sumar meses a días 29, 30 o 31: se desbordan al mes siguiente. Usa
last day of next monthcuando proceda. - Calcular con segundos a mano (
86400por día): falla en los cambios de hora de marzo y octubre. UsaDateIntervalydiff().
Resumen
| Tarea | Cómo |
|---|---|
| Zona horaria | date_default_timezone_set('Europe/Madrid') |
| Momento actual | time() o new DateTimeImmutable() |
| Formatear | date('d/m/Y H:i') o $fecha->format('d/m/Y H:i') |
| Fecha concreta | mktime(h, i, s, mes, día, año) o new DateTimeImmutable('2026-03-05') |
| ¿Existe la fecha? | checkdate($mes, $dia, $anio) |
| Sumar y restar | ->modify('+1 week'), ->add(new DateInterval('P1M')), ->sub(...) |
| Diferencia | $a->diff($b)->days, ->y, ->format('%a') |
| Comparar | $a < $b |
| Recorrer días | new DatePeriod($inicio, new DateInterval('P1D'), $fin) |
| Leer un formato | DateTimeImmutable::createFromFormat('!d/m/Y', $texto) |
| En español | Arrays de nombres o IntlDateFormatter |
| Base de datos | Guardar como Y-m-d H:i:s |
En la siguiente lección empezarás a recibir datos del usuario: formularios GET y POST.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.