Saltar al contenido
fechas.php · devschool

Fechas y horas en PHP

Lección 13 de 26 · 14 min de lectura · Actualizado el

En esta lección
  1. Primero, la zona horaria
  2. El timestamp: una fecha como un número
  3. date(): dar formato a una fecha
  4. DateTimeImmutable: fechas como objetos
  5. Leer y validar fechas: createFromFormat
  6. Fechas en español
  7. Fechas y bases de datos
  8. Ejemplo completo: estado de un bono
  9. Errores frecuentes
  10. Resumen

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 directiva date.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:

LetraSignificadoEjemplo
dDía del mes con dos cifras05
jDía del mes sin cero inicial5
mMes con dos cifras03
nMes sin cero inicial3
YAño con cuatro cifras2026
yAño con dos cifras26
HHora de 00 a 2309
GHora de 0 a 23, sin cero9
iMinutos07
sSegundos45
NDía de la semana, de 1 (lunes) a 7 (domingo)4
wDía de la semana, de 0 (domingo) a 6 (sábado)4
tDías que tiene el mes31
L1 si el año es bisiesto, 0 si no0
D / lDía de la semana en inglés (corto / largo)Thu / Thursday
M / FMes 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 usa createFromFormat(), 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 es Y-m-d H:i:s.
  • format() usa las mismas letras que date(). T muestra 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() con strftime() 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 configura php.ini.
  • Confundir m con i: m es el mes; los minutos son i. date('H:m') muestra la hora y el mes.
  • Pasar fechas españolas a strtotime(): 05/03/2026 se lee como 3 de mayo. Usa createFromFormat('d/m/Y', ...).
  • Usar DateTime y modificar la fecha original sin querer: usa DateTimeImmutable.
  • 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 month cuando proceda.
  • Calcular con segundos a mano (86400 por día): falla en los cambios de hora de marzo y octubre. Usa DateInterval y diff().

Resumen

TareaCómo
Zona horariadate_default_timezone_set('Europe/Madrid')
Momento actualtime() o new DateTimeImmutable()
Formateardate('d/m/Y H:i') o $fecha->format('d/m/Y H:i')
Fecha concretamktime(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íasnew DatePeriod($inicio, new DateInterval('P1D'), $fin)
Leer un formatoDateTimeImmutable::createFromFormat('!d/m/Y', $texto)
En españolArrays de nombres o IntlDateFormatter
Base de datosGuardar 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

[PHP] ¿Qué fecha muestra este código?
$c = new DateTimeImmutable('2026-01-31');
echo $c->modify('+1 month')->format('d/m/Y');

[PHP] ¿Qué muestra este código?
$f = new DateTimeImmutable('2026-03-05');
$f->modify('+1 week');
echo $f->format('d/m');

[PHP] ¿Qué fecha muestra este código?
date_default_timezone_set('Europe/Madrid');
echo date('d/m/Y', strtotime('05/03/2026'));

[PHP] Son las 14:30 del 1 de julio de 2026. ¿Qué muestra date('H:m')?

¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.