Cómo tratar con fechas en PHP

El problema de las fechas

La razón de más de un refunfuño de cualquier programador (novato o experimentado), es el tratamiento de fechas.

La irregularidad de nuestro calendario, sumado a las diferentes formas de representación (Día-Mes-Año, Año-Mes-Día, etc…) hacen que algo que debería ser trivial se convierta en una gran pérdida de tiempo y frustración.

Esto sucede en casi cualquier lenguaje de programación y PHP no es la excepción… ¿o sí?

Fechas y PHP

Como de costumbre en PHP, las opciones son varias. Queda a criterio de cada desarrollador seleccionar, entre ellas, cuál es la más conveniente ante cada escenario.

Algunas opciones que se me vienen a la mente son:

  • Usar un array para almacenar cada dato de tipo fecha
  • Almacenar cada fecha como una cadena
  • Diseñar tu propia clase fecha
  • Almacenar la fecha como un entero (Como se hace con el tiempo unix)
  • Usar la librería DateTime (alerta de spoiler: de esto hablaré en este artículo :))

El principal problema que vas a encontrar si intentas usar tu propia estructura de datos es que los cálculos y comparaciones serán realmente un dolor de cabeza (tendrás que programar un montón de operaciones que ya están resueltas si usas algunas herramientas propias del lenguaje).

Algo que puede aliviarte un poco (si elegís esta opción) es usar la versión estructurada del manejo de fechas de PHP: las funciones getdate y familia.

No está mal como primer paso… pero puedes ir todavía un poco más allá y realmente aprovechar el poder que tienes entre manos.

La librería DateTime

DateTime es una librería que ya viene incorporada a PHP (Al menos a partir de la versión 5… teniendo en cuenta que al momento de la escritura de este artículo la versión recomendada es la 7.1, creo que puedo asumir sin mucho miedo a equivocarme que estás usando una versión que soporta este tipo de datos…).

Esta librería trae consigo unas cuantas clases sumamente interesantes. Entre ellas:

Veamos de qué se tratan y cómo podés beneficiarte de usar cada una.

DateTime

Esta es la clase que da nombre a toda la librería. Podríamos decir que es la principal.

Como te podrás imaginar, su objetivo es representar fechas en un formato compatible con POO.

Constructor

Su principal característica es la versatilidad de su constructor.

Podemos hacer algo muy convencional como construir una fecha de este modo:

$d = new DateTime("2017/12/22");

O bien algo un poco más loco como:

$d = new DateTime("now");

En este caso el objeto $d contendrá la fecha de hoy. Este efecto puede lograrse también invocando al constructor sin parámetros.

Pero lo que casi me hizo aplaudir de pié cuando lo descubrí es la posibilidad de usar cadenas escritas en lenguaje natural, como por ejemplo last day of this month (Y sí… hay que saber un poco de Inglés :).

Por ejemplo, estoy escribiendo esto el 12 de Julio de 2017 y al ejecutar este comando:

$d = new DateTime('last day of this month')

El objeto $d contiene la fecha 2017-06-30.

Otros ejemplos interesantes de cadenas que puedes usar para crear fechas son:

  • yesterday
  • last friday
  • third thursday of next month
  • -60 day

En fin, las posibilidades son realmente amplias… podés consultar una guía completa acá.

format

El método format te permite obtener una representación de una fecha como una cadena de caracteres, pero no siempre la misma cadena si no aquella que se ajuste a tu necesidad particular (Por ejemplo, podrías permitir a cada usuario de tu sitio decidir cómo quiere ver las fechas que les correspondan).

El modo de hacer esto consiste en pasar como parámetro al método format una cadena que especifique cómo debe estar conformada la cadena que debe ser generada.

Por ejemplo, para mostrar una fecha en formato día/mes/año harías algo como esto:

$d = new DateTime();
echo $d->format('d-m-y');

Si quisieras ver el nombre del mes (en lugar del número), podrías usar la cadena d-M-Y.

Si quisieras ver algo del estilo 9:56pm on Wednesday 12th July 2017 podrías usar la cadena 'g:ia \o\n l jS F Y'.

Las opciones son amplias, podés consultar ayuda acá.

Algo interesante a tener en cuenta es como esa característica tan controvertida de PHP que es la conversión automática entre tipos de datos compatibles (Por ejemplo, "2" y 2) puede ayudarte a hacer cálculos.

Pensá en un ejemplo como este:

$d = new DateTime();
$dia = $d->format('d');
$manana = $dia + 1;

add y sub

Estos métodos permiten adicionar o sustraer una unidad de tiempo a un objeto DateTime. Veremos más detalles en un momento (Cuando hablemos de la clase DateInterval).

diff

Este método permite calcular la diferencia entre una fecha y otra (Por ejemplo para poder calcular cuántos días faltan para tu cumpleaños):

$d = new DateTime();
$diff = $d->diff($fechaCumple);

El resultado de esta operación es un objeto de clase DateInterval.

DateInterval

Esta clase es especialmente útil cuando se trata de realizar cálculos sobre fechas.

Por ejemplo, si tuvieras que hacer un sistema que envíe algún tipo de recordatorio 30 días a partir de una fecha, necesitarías calcular a qué fecha corresponde ese día.

Para hacer esto usando estas dos clases puedes usar un código de este estilo:

$d->add(new DateInterval('P30D'));

Se explica por si mismo, ¿cierto? :)

Ok, voy a darte algo más de detalle: lo que se está haciendo es llamar al método add sobre $d (Asumamos que ese objeto contiene la fecha incial) pasándole como parámetro un nuevo objeto de clase DateInterval, el cual se construye utilizando la cadnea 'P30D'.

¿Por qué esa cadena en particular? Es una regla que usa esta clase (DateInterval) que significa lo siguiente:

  • La P es por Period (Período)
  • El 30 es la cantidad de unidades de tiempo
  • La D es por Days (Días)

Es decir, estamos construyendo un intervalo que corresponde a un período de 30 días.

Es posible construir intervalos de tiempo basados en meses, años, minutos, horas, etc… (Podés ver la información completa Acá).

Esto sólo ya debería convencerte de usar estas clases en lugar de cualquier otra alternativa disponible… ¿todavía no te convencí al 100%? Seguí leyendo :).

Típico problema de manejo de fechas es calcular el día siguiente de una fecha cualquiera (¿Y si es fin de mes? ¿Y si es fin de año?).

Con el método add y DateInterval todo está resuelto mágicamente:

$d->add(new DateInterval('P1D'));

Del mismo modo podés ir hacia atrás (Usando el método sub):

$d->sub(new DateInterval('P1D'));

format

Un método interesante que ofrece esta clase es format. Usándolo podés obtener una representación de un intervalo como una cadena:

$d = new DateTime();
echo $d->diff(new DateTime('first day of this month'))->format('%d dias');

DateTimeZone

Si te toca trabajar con diferentes zonas horarias a la vez no desesperes, la clase DateTimeZone está ahí para ayudarte.

Como siempre, la referencia obligada es php.net.

Un ejemplo ilustrativo:

$t1 = new DateTime("first day of this month", new DateTimeZone('Asia/Bahrain'));
$t2 = new DateTime("first day of this month", new DateTimeZone('America/Argentina/Buenos_Aires'));

echo ($t1->diff($t2))->format('%d dias').PHP_EOL;

Da como resultado 1 dias (Aparentemente Bahrain dista bastante de Buenos Aires :)

DatePeriod

Si te tocara desarrollar algún tipo de aplicación que necesitara contar con un conjunto de fechas separadas entre sí por algún valor fijo (Por ejemplo, algo que sucede una vez cada 20 días), te podría ser útil la clase DatePeriod.

De lo que se trata esta clase es, precisamente, de modelar de un modo sencillo una serie de fechas correlacionadas. Por ejemplo:

$i = new DateInterval('P1D');
$d1 = new Datetime();
$p = new DatePeriod($d1, $i, 5);

foreach($p as $d) {
    echo $d->format('Y-m-d H:i:s') . "\n";
}

Dará como resultado los próximos 5 días.

Otra forma de crear un intervalo es definiendo la fecha de inicio y la de fin:

$i = new DateInterval('P1D');
$d1 = new Datetime();
$d2 = clone $d1;
$d2->add(new DateInterval('P5D'));
$p = new DatePeriod($d1, $i, $d2);

foreach($p as $d) {
    echo $d->format('Y-m-d H:i:s') . "\n";
}

DateTimeImmutable

Otra clase que puede resultarte útil cuando manejes fechas en tu aplicación es DateTimeImmutable. DateTimeImmutable es prácticamente indistinguible de DateTime, excepto por el hecho de que los métodos que se invocan sobre sus instancias no las modifican (A diferencia de DateTime).

DateTimeInterface

Un problema con el que puedes encontrarte es el hecho de que las clases DateTime y DateTimeImmutable no son libremente intercambiables, con lo cual, si un método espera recibir un objeto fecha debés pasar el tipo específico.

Para evitar estas situaciones desagradables existe la interface DateTimeInterface. Si necesitas hablar genéricamente de fechas, podés usarla (Ya que es implementada tanto por DateTime como por DateTimeImmutable).

Esto es especialmente útil cuando estés creando tu propia función que necesita recibir una fecha y no importa si es o no modificable.

Un ejemplo real

Un problema bastante molesto que tenía con un desarrollo para un cliente de Leeway era el hecho de que se necesitaba, en cualquier momento, tener los datos del último trimestre.

Así es como calculé las fechas del intervalo:

$fechaUltimoMes = new DateTime('-1 month');
$m = $fechaUltimoMes->format('m');
$ultimoCuatrimestre = floor($fechaUltimoMes->format('m') / 4) + 1;
$anioUltimoCuatrimestre = $fechaUltimoMes->format('Y');

$desdeFecha = new \DateTime( $anioUltimoCuatrimestre.'-'.$m.'-01');
$hastaFecha = clone $desdeFecha;
$hastaFecha
  ->add(new \DateInterval('P3M'))
  ->sub(new \DateInterval('P1D'));

echo "Desde: ".$desdeFecha->format('d/m/y')." hasta: ".$hastaFecha->format('d/m/y').PHP_EOL;

Corriéndolo hoy el resultado que obtengo es Desde: 01/06/17 hasta: 31/08/17.

No está mal, ¿eh?

Una forma algo más eficiente

Si ves nuevamente el código que vengo usando, cuando hago add por ejemplo, el resultado de la operación es un objeto DateTime, pero no cualquier objeto si no el objeto sobre el que haya invocado el método en cuestión (Para permitir las operaciones encadenadas).

Otra opción para lograr el mismo resultado sería esta:

$fechaUltimoMes = new DateTime('-1 month');
$m = $fechaUltimoMes->format('m');
$ultimoCuatrimestre = floor($fechaUltimoMes->format('m') / 4) + 1;
$anioUltimoCuatrimestre = $fechaUltimoMes->format('Y');

$desdeFecha = new \DateTimeImmutable( $anioUltimoCuatrimestre.'-'.$m.'-01');
$hastaFecha = $desdeFecha
  ->add(new \DateInterval('P3M'))
  ->sub(new \DateInterval('P1D'));

echo "Desde: ".$desdeFecha->format('d/m/y')." hasta: ".$hastaFecha->format('d/m/y').PHP_EOL;

En este caso el resultado de las operaciones add y sub no es un DateTime si no un DateTimeImmutable.

Conclusión

Como habrás visto, el manejo de fechas que PHP pone a tu disposición es realmente versátil y poderoso… no pierdas tu tiempo reinventando la rueda, aprende a sacarle el máximo provecho a la herramienta que estás usando.

¿Cuál ha sido tu desafío más importante cuando te tocó tratar con fechas en PHP? ¿Encontraste algo en este artículo que te habría hecho la vida más fácil? ¡Coméntalo!