Cálculos avanzados de fecha y hora en PHP con DateTime
Trabajar con fechas en PHP es una de esas tareas que parecen simples hasta que te toca calcular el último día del mes, sumar días hábiles a una fecha, convertir una zona horaria o generar el rango de los últimos seis meses para un reporte. Las funciones nativas tipo date() resuelven lo básico, pero cuando el proyecto crece necesitas algo más robusto.
En este post te voy a mostrar dos approaches: por un lado la API moderna de DateTime y DateTimeZone que trae PHP, y por el otro una clase helper que tengo en mi repositorio personal desde hace años y que uso en prácticamente todos los proyectos. La llamo Fechas y la armé justamente para no estar reinventando la rueda cada vez que tengo que formatear una fecha en español, sumar días hábiles o convertir entre zonas horarias.
El problema con las funciones nativas sueltas
Si alguna vez tuviste que hacer algo más allá de un date('Y-m-d'), seguro te topaste con esta clase de código:
$fecha = "2026-06-27";
$partes = explode('-', $fecha);
$mesEnTexto = array('Enero', 'Febrero', ...)[(int)$partes[1] - 1];
echo $partes[2] . " de " . $mesEnTexto . " del " . $partes[0];
Funciona, pero es feo, repetitivo y está lleno de números mágicos. Si lo copias en cinco archivos distintos, el día que cambies el formato del mes, vas a tener que cazar cada copia. Y ni hablar de los bugs clásicos: usar strtotime con una fecha ambigua, olvidar configurar la zona horaria y obtener horas fantasma, o calcular mal el último día de febrero en año bisiesto.
La API de DateTime te da objetos con métodos claros y zona horaria explícita, lo cual ya es una mejora enorme. Pero aún así, en proyectos reales uno termina creando funciones de envoltura porque la sintaxis se vuelve verbosa para cosas que haces todo el tiempo. Por eso armé la clase Fechas.
La clase Fechas: un helper reutilizable
La idea es tener un lugar centralizado con todas las operaciones de fecha que repito en mis proyectos. La clase vive en el namespace App\Helpers y todos sus métodos son static, así la uso sin necesidad de instanciar nada.
<?php
namespace App\Helpers;
use DateTime;
use DateTimeZone;
class Fechas
{
private static $dayOfWeek = array('Domingo', 'Lunes', 'Martes', 'Miercoles', 'Jueves', 'Viernes', 'Sabado');
private static $monthOfYear = array('Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre');
}
Los dos arrays estáticos al inicio guardan los nombres en español, que es lo que más uso. En lugar de pelearme con IntlDateFormatter o setlocale cada vez, los tengo ahí disponibles y puedo abreviarlos cuando los necesito para una interfaz compacta (por ejemplo, “Lun 27 de Jun”).
Obtener fecha y hora actual
Empiezo por lo más básico: cómo pedirle al sistema la fecha y la hora en distintos formatos. Estos métodos son pequeños wrappers sobre date(), pero el valor real es que estandarizan el formato en todo el proyecto.
echo Fechas::getDate();
// "2026-06-27" (formato ISO, ideal para guardar en BD)
echo Fechas::getDate("es");
// "27-06-2026" (formato latino)
echo Fechas::getHour();
// "14:32:08"
echo Fechas::getDateTime("es");
// "27-06-2026 14:32:08"
El método getDate acepta un parámetro type con valor "es" para invertir día y mes. Es un parámetro muy pequeño pero útil: en formularios de países hispanohablantes solemos esperar dd-mm-aaaa y en bases de datos siempre guardamos aaaa-mm-dd. Tener una sola función que cubre ambos casos evita que el equipo invente sus propias convenciones.
Nombres de días y meses en español
Esta es la parte que más uso. En lugar de tener un array perdido en cada vista, lo centralizo aquí.
Fechas::getNameDay(1); // "Lunes"
Fechas::getNameDay(1, 3); // "Lun"
Fechas::getNameDay(1, 2); // "Lu"
Fechas::getNameMonth(5); // "Junio"
Fechas::getNameMonth(5, 3); // "Jun"
Fechas::getNameMonth(5, 2); // "Ju"
El parámetro avr controla la abreviatura: 3 caracteres para algo legible en una columna de tabla, 2 caracteres para interfaces muy compactas como badges o chips. null (el valor por defecto) devuelve el nombre completo.
Hay un detalle interesante en la implementación. Para abreviar usa substr con un cálculo de longitud:
$long = strlen(self::$dayOfWeek[$num]);
$day = substr(self::$dayOfWeek[$num], 0, -($long - 3));
Es un truco para no hardcodear la longitud de cada palabra. Si mañana cambio "Miercoles" por "Miércoles" (con tilde), la abreviatura sigue funcionando porque recalcula el corte. Funciona, aunque si alguna vez agregas palabras de menos de 3 caracteres te va a explotar (cosa rara en días y meses, pero ojo).
Formatear fechas en texto legible
Una necesidad muy común en backoffice y reportes es mostrar la fecha en formato humano, no 2026-06-27 sino “27 de Junio del 2026”. Eso lo resuelve getDateText.
echo Fechas::getDateText();
// "27 de Junio del 2026"
echo Fechas::getDateText('/', '2026-06-27', 3);
// "27/Jun/2026"
echo Fechas::getDateText('txt', '2026-06-27', 2);
// "27 de Ju del 2026"
El primer parámetro define el separador ('txt' para texto natural con “de” y “del”, o cualquier otro string como '/' o '-'). El segundo es la fecha (si lo dejas en null toma la actual) y el tercero es la abreviatura del mes.
También hay una variante que incluye la hora, útil para logs o timestamps de actividades:
echo Fechas::getDateHourText('2026-06-27 14:32:08');
// "27-Junio-2026 a 02:32"
Notá que la hora se convierte de 24 a 12 horas, lo cual es típico para mostrar al usuario final. Si necesitás la hora en formato 24, podés modificarla cambiando la línea $dateText .= Fechas::getHours($hora[0]) . ':' . $hora[1]; por $dateText .= $hora[0] . ':' . $hora[1];.
Sumar y restar días, meses y años
Otra operación del día a día. Calcular vencimientos, fechas de corte, plazos de gracia. Acá los métodos están espejados: addDay y downDay, addMonth y downMonth, addYear y downYear.
$hoy = "2026-06-27";
echo Fechas::addDay($hoy, 15);
// "2026-07-12"
echo Fechas::downDay($hoy, 7);
// "2026-06-20"
echo Fechas::addMonth($hoy, 3);
// "2026-09-27"
echo Fechas::addYear($hoy, 1);
// "2027-06-27"
La implementación usa mktime y list para descomponer la fecha. Hay que tener presente un detalle: si hacés addDay("2026-01-31", 1), el resultado es "2026-02-01" porque PHP normaliza automáticamente la fecha. Esto suele ser lo que querés, pero si necesitás mantener el día del mes original, deberías trabajar con objetos DateTime que tengan métodos como modify('+1 day') con comportamiento más predecible.
Acá es donde entra la API moderna de PHP, que es más explícita sobre ese tipo de cosas:
$fecha = new DateTime("2026-01-31");
$fecha->modify('+1 day');
echo $fecha->format('Y-m-d'); // "2026-02-01"
Mismo resultado, pero queda más claro qué estás haciendo.
Calcular días entre dos fechas
Para calcular la diferencia entre dos fechas hay un método específico. Lo armé para reportes de antigüedad, tiempo transcurrido desde una compra, o cualquier validación tipo “esto no puede tener más de N días”.
echo Fechas::getDaysPassed("2026-01-15", "2026-06-27");
// 163
La implementación toma ambas fechas, las convierte a timestamp Unix, las resta en segundos, divide por 86400 (segundos en un día) y devuelve el valor absoluto como entero. Si la fecha final es menor que la inicial, lanza una excepción.
Un detalle interesante: el cálculo usa floor, lo que significa que devuelve días completos. Si querés incluir fracciones de día (por ejemplo para calcular edades o tiempos de servicio), deberías quitar el floor y devolver un float. También es importante notar que ignora las horas, así que si pasás "2026-01-15 23:00" y "2026-01-16 01:00" te va a decir que pasó 1 día, no 2 horas. Para casos donde la hora importa, conviene normalizar ambas fechas a Y-m-d antes de pasarlas.
Último día del mes
Esta es una operación que parece trivial pero esconde varios casos especiales. Febrero puede tener 28 o 29 días según si el año es bisiesto. Los meses de 30 y 31 días hay que recordarlos a mano. Por eso vale la pena centralizarla.
echo Fechas::getLastDayMonth("2026-02-15");
// "2026-02-28"
echo Fechas::getLastDayMonth("2024-02-15");
// "2024-02-29" (2024 es año bisiesto)
echo Fechas::getLastDayMonth("2026-04-15");
// "2026-04-30"
El truco de la implementación es saltar al primer día del mes siguiente y restar un segundo. No es la forma más elegante, pero funciona y no necesita de una tabla de días por mes. Como bonus, devuelve la fecha en formato Y-m-d, lista para insertar en una base de datos o comparar con otras fechas.
Convertir entre zonas horarias
Acá es donde entra la API moderna de PHP. Para trabajar con zonas horarias necesitás dos clases: DateTime para la fecha y DateTimeZone para la zona destino.
echo Fechas::convertDateIsoToTimeZone("2026-06-27 14:00:00", "America/Mexico_City");
// "2026-06-27 08:00:00"
El método toma la fecha (asume que está en la zona horaria por defecto del servidor) y la convierte a la zona horaria destino usando DateTime::setTimezone. La lista de zonas válidas la podés sacar de la documentación oficial de PHP, que incluye todas las zonas IANA.
Un caso de uso típico: tu servidor está en UTC y guardás todo en UTC, pero necesitás mostrarle al usuario la hora en su zona local. Si tenés la zona del usuario guardada en su perfil, esto es directo:
$zonaUsuario = "America/Buenos_Aires";
echo Fechas::convertDateIsoToTimeZone($evento->created_at, $zonaUsuario);
Importante: para que esto funcione correctamente, la zona horaria del servidor tiene que estar bien configurada con date_default_timezone_set en el bootstrap de la aplicación, o seteada en el php.ini. Si la fecha de entrada ya tiene la zona incluida (formato ISO 8601 con offset como 2026-06-27T14:00:00+00:00), PHP la respeta automáticamente.
Sumar días hábiles
Esta función la armé para sistemas de gestión donde los plazos se cuentan solo en días laborables. Por ejemplo, “entrega en 5 días hábiles” o “vencimiento en 10 días hábiles a partir de hoy”.
echo Fechas::addWeekDay(5, "2026-06-26");
// "Mie 01 de Julio"
El primer parámetro es la cantidad de días hábiles a sumar y el segundo es la fecha de inicio. Si no pasás fecha, toma la actual. El formato de salida incluye el nombre abreviado del día y el mes completo, ideal para mostrar en una pantalla de “fecha estimada de entrega”.
La lógica de cálculo es interesante: toma el día de la semana de la fecha de inicio (1 para lunes, 7 para domingo en formato ISO), suma los días solicitados, calcula cuántos fines de semana completos hay en el medio y los suma al total. Después agrega esa cantidad de segundos a la fecha original. El truco está en los casos especiales de los bordes (cuando caés justo en sábado o domingo) que están manejados con los if que ajustan $findesemana.
Hay una versión “raw” que devuelve la fecha sin formatear, útil cuando solo necesitás el cálculo:
echo Fechas::getNexBusinessDay(5, "2026-06-26");
// "2026-07-01"
Listar los últimos N meses
Para reportes con un selector de período, muchas veces necesitás generar un array con los últimos 6 o 12 meses y sus nombres, así el usuario puede elegir. Eso es lo que hace getListMonthPast:
$meses = Fechas::getListMonthPast();
foreach ($meses as $mes) {
echo $mes['name'] . " - " . $mes['date'] . "\n";
}
// Junio - 2026-06
// Mayo - 2026-05
// Abril - 2026-04
// Marzo - 2026-03
// Febrero - 2026-02
// Enero - 2026-01
Itera desde el mes actual hacia atrás, generando el par nombre + periodo. Es ideal para llenar un select en un formulario de filtros, y la primera entrada siempre es el mes en curso. Si necesitás solo 3 meses, podés cortar con array_slice($meses, 0, 3).
Cuándo conviene usar DateTime puro
La clase Fechas resuelve el 90% de los casos que me encuentro, pero hay situaciones donde conviene ir directo a DateTime:
- Cuando necesitás hacer cálculos con locales específicos (por ejemplo, fechas en japonés con
IntlDateFormatter). - Cuando operás con intervalos (
DateIntervalpermite hacer diff entre fechas y obtener años, meses y días por separado, no solo total de días). - Cuando el formato de entrada es ambiguo y querés ser estricto con
DateTime::createFromFormaty manejar errores de parseo. - Cuando hacés muchas operaciones encadenadas sobre la misma fecha (sumar, restar, formatear varias veces), porque mantener una instancia es más eficiente que parsear strings una y otra vez.
Un ejemplo concreto con DateTime y DateInterval que la clase Fechas no cubre:
$inicio = new DateTime("2026-01-15");
$fin = new DateTime("2026-06-27");
$diff = $inicio->diff($fin);
echo $diff->y . " años, " . $diff->m . " meses, " . $diff->d . " días";
// "0 años, 5 meses, 12 días"
Esto es útil para calcular edades o antigüedad en formato “X años, Y meses”, algo común en sistemas de recursos humanos o contratos.
Conclusión
La regla que sigo es: para el 90% de operaciones repetidas en mis proyectos, tener una clase helper como Fechas me ahorra tiempo y mantiene consistencia. Para cálculos más complejos, cálculos con locales específicos, o cuando necesito el poder completo de la API moderna, voy directo a DateTime y DateInterval.
Lo que recomiendo es no empezar con la clase completa el día uno. Empezá con dos o tres métodos que más uses, sumá los que vayas necesitando, y refactorizá a DateTime cuando te topes con un caso que el helper no cubra bien. La idea es tener un toolkit propio que crezca con tu experiencia, no un mostruo de 30 métodos que no usás.
En el próximo post vamos a meternos con arrays, y después con el manejo de sesiones y cookies. Si tenés tu propia clase helper o alguna función de fecha que uses seguido, me encantaría leerla en los comentarios.