Curso de PHP

Fechas y horas en PHP: DateTime, formatos y zonas horarias

Por Víctor Peña · Publicado el

Hola, ¿cómo están? Cuarto tema complementario del curso de PHP. Las fechas parecen algo trivial hasta que aparecen las zonas horarias, los meses de distinta duración y los años bisiestos.

¡Empecemos!

Empieza por configurar la zona horaria

Lo primero de todo, al inicio de tu aplicación:

<?php
date_default_timezone_set('America/La_Paz');

Sin esto, PHP usa la zona configurada en el servidor, que rara vez es la tuya. Es el origen de la mitad de los problemas con fechas: la hora que ves en local no coincide con la del servidor de producción.

La función date

Para lo sencillo, date() alcanza:

<?php
echo date('Y-m-d');           // 2026-03-01
echo date('d/m/Y H:i');       // 01/03/2026 14:30
echo date('l, j \d\e F Y');   // Sunday, 1 de March 2026

Los formatos más usados:

Letra Significado Ejemplo
Y / y Año 2026 / 26
m / n Mes con o sin cero 03 / 3
d / j Día con o sin cero 01 / 1
H / h Hora 24 o 12 14 / 02
i Minutos 30
s Segundos 45
l / D Día de la semana Sunday / Sun
F / M Mes en texto March / Mar

DateTimeImmutable: la forma correcta

Para todo lo que no sea imprimir la fecha actual, usa objetos. Y concretamente DateTimeImmutable, no DateTime.

<?php
$hoy = new DateTimeImmutable();
$fecha = new DateTimeImmutable('2026-03-15');
$otra = new DateTimeImmutable('2026-03-15 14:30:00');

echo $fecha->format('d/m/Y');   // 15/03/2026

Por qué inmutable

Esta diferencia causa errores muy difíciles de encontrar:

<?php
// DateTime: se modifica a sí mismo
$fecha = new DateTime('2026-03-01');
$vencimiento = $fecha->modify('+30 days');

echo $fecha->format('Y-m-d');        // 2026-03-31  ← ¡cambió!
echo $vencimiento->format('Y-m-d');  // 2026-03-31
<?php
// DateTimeImmutable: devuelve uno nuevo
$fecha = new DateTimeImmutable('2026-03-01');
$vencimiento = $fecha->modify('+30 days');

echo $fecha->format('Y-m-d');        // 2026-03-01  ← intacta
echo $vencimiento->format('Y-m-d');  // 2026-03-31

Con DateTime, pasar la fecha a una función que la modifica cambia tu variable original sin avisar. Usa siempre DateTimeImmutable salvo que tengas una razón muy concreta.

Fechas relativas

PHP entiende expresiones en inglés que resultan muy cómodas:

<?php
new DateTimeImmutable('tomorrow');
new DateTimeImmutable('+1 week');
new DateTimeImmutable('first day of next month');
new DateTimeImmutable('last day of this month');
new DateTimeImmutable('next monday');

Sumar y restar tiempo

<?php
$hoy = new DateTimeImmutable();

$en30dias = $hoy->modify('+30 days');
$haceUnAnio = $hoy->modify('-1 year');

// O con DateInterval, más explícito
$vencimiento = $hoy->add(new DateInterval('P30D'));   // P = periodo, 30 D = días
$antes = $hoy->sub(new DateInterval('P1M'));          // 1 mes

El formato de DateInterval es raro pero se aprende rápido: P inicia el periodo, T separa la parte de tiempo. P1Y2M3DT4H5M es un año, dos meses, tres días, cuatro horas y cinco minutos.

Calcular diferencias

<?php
$inicio = new DateTimeImmutable('2026-01-15');
$fin = new DateTimeImmutable('2026-03-01');

$diferencia = $inicio->diff($fin);

echo $diferencia->days;    // 45  ← total de días
echo $diferencia->m;       // 1   ← meses de la diferencia
echo $diferencia->d;       // 14  ← días restantes tras los meses
echo $diferencia->format('%m meses y %d días');

Ojo con la distinción: days es el total, mientras que m y d son las partes descompuestas. Confundirlos es un error clásico.

Calcular una edad

<?php
function calcularEdad(string $nacimiento): int
{
    $fecha = new DateTimeImmutable($nacimiento);
    return $fecha->diff(new DateTimeImmutable())->y;
}

echo calcularEdad('1990-06-15');

diff() se encarga de los años bisiestos y de si ya pasó el cumpleaños este año. No calcules edades restando años a mano.

Comparar fechas

Los objetos de fecha se comparan directamente:

<?php
$vencimiento = new DateTimeImmutable('2026-03-15');
$hoy = new DateTimeImmutable();

if ($vencimiento < $hoy) {
    echo "Vencido";
}

Fechas en español

format() devuelve los nombres en inglés. Para español, usa IntlDateFormatter:

<?php
$formateador = new IntlDateFormatter(
    'es_ES',
    IntlDateFormatter::LONG,
    IntlDateFormatter::NONE
);

echo $formateador->format(new DateTimeImmutable());
// 1 de marzo de 2026

Requiere la extensión intl, que suele venir activada. Si no la tienes, la alternativa es un array de traducción, pero intl maneja además los formatos propios de cada región.

Fechas y base de datos

Guarda siempre en el formato que MySQL entiende:

<?php
$fecha = new DateTimeImmutable();

$sentencia = $pdo->prepare("INSERT INTO pedidos (creado_en) VALUES (:fecha)");
$sentencia->execute(['fecha' => $fecha->format('Y-m-d H:i:s')]);

Y al leer:

<?php
$pedido = $sentencia->fetch();
$creado = new DateTimeImmutable($pedido['creado_en']);
echo $creado->format('d/m/Y');

Nunca guardes fechas como texto en formato local. Un 15/03/2026 en la base de datos no se puede ordenar ni comparar correctamente, y además es ambiguo: en otros países eso sería el 3 de quince.

Zonas horarias

Si tu aplicación tiene usuarios en varios países, la práctica recomendada es guardar todo en UTC y convertir al mostrar:

<?php
// Guardar en UTC
$ahora = new DateTimeImmutable('now', new DateTimeZone('UTC'));

// Mostrar en la zona del usuario
$local = $ahora->setTimezone(new DateTimeZone('America/La_Paz'));
echo $local->format('d/m/Y H:i');

Validar una fecha

Cuando la fecha llega de un formulario, no confíes en que sea válida:

<?php
function esFechaValida(string $valor, string $formato = 'Y-m-d'): bool
{
    $fecha = DateTimeImmutable::createFromFormat($formato, $valor);

    return $fecha && $fecha->format($formato) === $valor;
}

var_dump(esFechaValida('2026-03-15'));   // true
var_dump(esFechaValida('2026-02-30'));   // false, febrero no tiene 30

La segunda comprobación es la importante: PHP acepta 2026-02-30 y lo convierte al 2 de marzo. Comparar el resultado con la entrada detecta esos casos.

Un ejemplo práctico

<?php
declare(strict_types=1);

date_default_timezone_set('America/La_Paz');

class Suscripcion
{
    public function __construct(
        private readonly DateTimeImmutable $inicio,
        private readonly int $mesesDuracion,
    ) {}

    public function vencimiento(): DateTimeImmutable
    {
        return $this->inicio->add(new DateInterval("P{$this->mesesDuracion}M"));
    }

    public function diasRestantes(): int
    {
        $hoy = new DateTimeImmutable();
        $vence = $this->vencimiento();

        if ($vence < $hoy) {
            return 0;
        }

        return $hoy->diff($vence)->days;
    }

    public function estaVencida(): bool
    {
        return $this->vencimiento() < new DateTimeImmutable();
    }
}

$sub = new Suscripcion(new DateTimeImmutable('2026-01-15'), 6);

echo $sub->vencimiento()->format('d/m/Y') . "\n";   // 15/07/2026
echo $sub->diasRestantes() . " días restantes\n";
var_dump($sub->estaVencida());

Errores comunes

  • No configurar la zona horaria y encontrarte con horas desplazadas en producción.
  • Usar DateTime en lugar de DateTimeImmutable y modificar sin querer la fecha original.
  • Guardar fechas como texto en formato local.
  • Confundir days con d en una diferencia.
  • Calcular edades restando años, sin considerar si ya pasó el cumpleaños.
  • Confiar en que una fecha de formulario es válida.

Para cerrar

Las fechas dan más problemas de los que parece, y casi todos se evitan con tres hábitos: configurar la zona horaria, usar DateTimeImmutable y guardar siempre en formato Y-m-d H:i:s.

En el último artículo veremos cómo consumir APIs externas.

Saludos y éxitos.

Norvic Software

Desarrollamos el software que tu empresa necesita

Somos una fábrica de software en Bolivia. Construimos sistemas a medida y aplicaciones móviles, y llevamos Inteligencia Artificial a las empresas que ya tienen un sistema funcionando.

Solicitar cotizaciónVer todos los servicios

Cotización sin costo · Respuesta directa por WhatsApp