La génération de dates en PHP

Par Kevin Nadin, conférencier Padawan / @kevinjhappy - Forum PHP 2017

Vraiment ??

Une présentation sur les dates ??

  • Oui, les dates sont piégeuses!
  • Il y a beaucoup de possibilités de les générer, et elles peuvent vous causer des soucis...

Nous allons voir de suite :

  1. La fonction date (forcément)
  2. La fonction mktime
  3. La fonction strtotime
  4. L'objet DateTime
  5. L'objet DateTimeZone
  6. L'objet DateInterval
  7. Des exemples d'utilisation

La fonction date()

Commençons avec les bases :

  • La première fonction à laquelle on pense, évidemment
  • Elle l'a été pour moi pour en générer
  • php.net: fonction date
$testDate = date('Y-m-d');

echo $testDate; 

// 2017-05-26


$testDate = date('Y-m-d H:i:s');

echo $testDate; 

// 2017-05-26 16:25:08

Les dates avec un timestamp

date() accepte un timestamp comme second paramètre

string date ( string $format [, int $timestamp = time() ] )

Timestamps ??

Un timestamp unix est le nombre de secondes écoulées depuis le
1er Janvier 1970 sans fuseau horaire

Vient des normes de l'IEEE qui ont standardisé cette date en marquant le début de l'ère UNIX

La fonction mktime()

Prends en entrée 6 paramètres :

Attention à leur ordre !

  1. Heures
  2. Minutes
  3. Secondes
  4. Mois
  5. Jours
  6. Années
$hour = 0;
$minute = 0;
$second = 0;
$month = 10;
$day = 24;
$year = 2016;

$timestamp = mktime($hour, $minute, $second, $month, $day, $year);

$myDate = date('Y-m-d', $timestamp);

echo $myDate; // 2016-10-24

mktime semble un peu moisi...

Ouais c'est vrai, cela peut être très compliqué.

Mais cela peut apporter une précision chirurgicale

// I want to get the last day of last month, we are the 26th May 2017

$month = date('m') - 1;
$year = date('Y');

// get the timestamp of last month
$timestampLastMonth = mktime(0, 0, 0, $month, 1, $year);

// get the last day of the month, so the 30th April
$lastDayOfTheMonth = date('t', $timestampLastMonth);

$timestampLastDayOfLastMonth = mktime(
	0, 0, 0, $month, $lastDayOfTheMonth, $year
	);

// I have the correct timestamp, I can call the function date
$lastDayOfLastMonth = date('Y-m-d', $timestampLastDayOfLastMonth);

echo $lastDayOfLastMonth; // 2017-04-30
//  I want to get the last day of last month, we are the 26th May 2017
// made in one line

$lastDayOfLastMonth = date(
    'Y-m-d', 
    mktime( 
        0, 
        0, 
        0, 
        date('m') - 1, 
        date('t',
	      mktime(
		0,
		0,
		0,
		date('m') - 1,
		1,
		date('Y')
		)),
        date('Y')
    )
);
// => 2017-04-30
Not sure if its php or matrix source code

Comportement à connaitre

Si j'essaye de mettre le 33 janvier

echo date('Y-m-d', mktime(0, 0, 0, 1, 33, 2017));

// 2017-02-02

Cela va prendre la date du 31 janvier + 2 jours

why-it-doesnt-send-me-a-fatal-error

La fonction strtotime()

Supposons, nous sommes le 2017-05-26 :

$firstDayOfLastMonth = date('Y-m-d', strtotime("first day of last month"));

// => 2017-04-01
$lastDayOfLastMonth = date('Y-m-d', strtotime("last Day of Last Month"));

// => 2017-04-30
$lastSunday = date('Y-m-d', strtotime("last Sunday"));

// => 2017-05-21
$mondayOfLastWeek = date('Y-m-d', strtotime("last Monday of Last Week"));

// => 2017-05-15

Alors strtotime est bien ?

  • Ouais, c'est pas mal du tout !
  • Il faut faire attention à la "magie" de la fonction
  • Certaines dates ne peuvent pas être faites avec
  • php.net : Formats relatifs
// WARNING !! first Specific day of last week
// will not give you what you want

$date = date('Y-m-d', strtotime("First Monday of Last Week"));

// => 2017-04-24
// WARNING !! if you are the 31th, Last month will give you 
// the day one of this month

$timeThirtyOneOctober = mktime(0,0,0,10,31,2017);

$wrongDate = date('Y-m-d', strtotime("Last Month", $timeThirtyOneOctober));

// => 2017-10-01
// Only way to get it right is by using this
$goodDate = date('Y-m-d', strtotime(
	"Last Day of Last Month", 
	$timeThirtyOneOctober
	));

// => 2017-09-30
// WARNING, some sentences may seem ok but do not work at all

$wrongDate = date('Y-m-d', strtotime("Monday of Last Week"));

// => 1970-01-01
// You can't construct with strtotime the 13th of this month

$dayThirteenOfThisMonth = date(
	'Y-m-d',
	mktime(0, 0, 0, date('m'), 13, date('Y'))
);
// => 2017-05-13
traps-traps-everywhere

L'objet DateTime

Ah ben enfin on discute !!

On peut oublier les chapitres précédents !

Yeah !!

minions yeah

Hum... NON !!

Parce que les comportements sont les mêmes !

$firstDayOfThisMonth = new \DateTime('first day of this month');

echo $firstDayOfThisMonth->format('Y-m-d');
// 2017-05-01
$dayFifteenOfThisMonth = new \DateTime();

$dayFifteenOfThisMonth->setTime(0, 0, 0);

$dayFifteenOfThisMonth->setDate(
    (int) $dayFifteenOfThisMonth->format('Y'),
    (int) $dayFifteenOfThisMonth->format('m'),
    15
);

echo $dayFifteenOfThisMonth->format('Y-m-d');
// 2017-05-15

Et les pièges sont les mêmes !

it's a trap !
// first Specific day of last Week
// Today is 2017-05-26

$date = new DateTime("First Monday of Last Week");

echo $date->format('Y-m-d') ;
// 2017-04-24
// sentences that may seems ok but does not work at all

$wrongDate = new DateTime("Monday of Last Week");

echo $wrongDate->format('Y-m-d');

// Fatal error: Uncaught Exception: DateTime::__construct():
// Failed to parse time string (Monday of Last Week) 
// WARNING !! if you are the 31th, Last month will give you
// the day one of this month

$lastMonthDate = new DateTime("2017-10-31");

$lastMonthDate->modify('Last Month');

echo $lastMonthDate->format('Y-m-d');
// 2017-10-01
// Only way to get it right is by using this
$lastMonthDate = new DateTime("2017-10-31");

$lastMonthDate->modify('Last Day of Last Month');

echo $lastMonthDate->format('Y-m-d');
// 2017-09-30

Petite parenthèse : correction possible avec la librairie Carbon

//Taking the last month of a 31th of this month
$date = Carbon::createFromDate(2016, 10, 31);

Carbon::useMonthsOverflow(false);

$date->subMonth(1);

echo $date->format('Y-m-d');
// 2016-09-30

Parlons aussi de DateTimeImmutable

  • Même utilisation que DateTime()
  • Différence : ne peut pas être modifié une fois créé.
  • Toute modification renvoit un nouvel objet
$date = new DateTimeImmutable("2017-05-26");

$newDate = $date->setDate(2017, 07, 12);

echo $date->format('Y-m-d'), " <=> ", $newDate->format('Y-m-d');
// 2017-05-26 <=> 2017-07-12

Une fonction en plus :

DateTimeImmutable::createFromMutable

// create a specific date
$date = new DateTime('2017-09-15');

$myNewDate = DateTimeImmutable::createFromMutable($date);

echo $myNewDate->format('Y-m-d');
// 2017-09-15

L'objet DateTimeZone

Toujours penser et faire attention avec le fuseau horaire

  • Par défaut configurez le serveur à UTC
  • Pensez à sauvegarder celui des dates reçues
  • N'oubliez pas de l'appliquer lorsque vous communiquez
// set a date with a Timezone
$date = new DateTime('2017-05-01', new DateTimeZone('Europe/Paris'));

echo "Europe/Paris " , $date->format('Y-m-d H:i:s P');
// Europe/Paris 2017-05-01 00:00:00 +02:00

$date->setDate(2017, 01, 01);

echo "Europe/Paris en hiver " , $date->format('Y-m-d H:i:s P');
// Europe/Paris en hiver 2017-01-01 00:00:00 +01:00

$date->setTimezone(new DateTimeZone('Australia/Sydney'));

echo "Australia/Sydney " , $date->format(\DateTime::ISO8601);
// Australia/Sydney 2017-01-01T10:00:00+1100

Fuseaux Horaires possibles

  • php.net => timezones : Attention à celles qui sont listés dans la catégorie "Autres" comme Japan, Turkey, etc...
  • Wikipedia.org => list timezones
// set a date with a Timezone in number of hours

$date = new DateTime('2017-05-01', new DateTimeZone('+02:00'));

echo "UTC + 2 hours " , $date->format('Y-m-d H:i:sP') ;

// UTC + 2 hours 2017-05-01 00:00:00+02:00


$utcDate = new DateTime('2017-05-01', new DateTimeZone('UTC'));

echo "UTC date " , $utcDate->format('Y-m-d H:i:sP');

// UTC date 2017-05-01 00:00:00+00:00

Et bien sûr... des pièges !

// today in Paris, let's try to get the location of the timezone
$dateInParis = new DateTime('now', new \DateTimeZone('Europe/Paris'));

print_r($dateInParis->getTimezone()->getLocation());
/*Array (
    [country_code] => FR
    [latitude] => 48.86666
    [longitude] => 2.33333
    [comments] =>
)*/
// now if we received this date in string format

$newDateInParis = new DateTime($dateInParis->format('Y-m-d H:i:sP'));

var_dump($newDateInParis->getTimezone()->getLocation());

// bool(false)

Pour éviter ce piège

// today in Paris, let's try to get the location of the timezone

$dateInParis = new DateTime('now', new \DateTimeZone('Europe/Paris'));

$newDateInParis = new DateTime(
    $dateInParis->format('Y-m-d H:i:s'),
    $dateInParis->getTimezone()
);

print_r($newDateInParis->getTimezone()->getLocation());

/* Array (
    [country_code] => FR
    [latitude] => 48.86666
    [longitude] => 2.33333
    [comments] =>
)*/

l'object DateInterval

Cela permet de définir une période

  • Seulement 2 fonctions, assez similaires
  • En utilisation DateTime->diff, cela donnera un intervale

new DateInterval('P2Y4DT6H8M')

Quelle est cette chose ??????

  • Le P est pour "Period", on précise des années aux jours
  • Le T est pour "Time", on précise des heures aux secondes
  • Dans notre exemple ci dessus :
    P: 2Year 4Day
    T: 6Hour 8Minute
// set a date Interval

$dateInterval = new DateInterval('P2Y4DT6H8M');

echo $dateInterval->format('%y years, %d days and %h hours, %i minutes');
// 2 years, 4 days and 6 hours, 8 minutes

// equals to :

$dateInterval = DateInterval::createFromDateString(
    '2 year + 4 day + 6 hour + 8 minutes'
);

echo $dateInterval->format('%y years, %d days and %h hours, %i minutes');
// 2 years, 4 days and 6 hours, 8 minutes

Quelques situations rencontrés

Demande simple : extraire les 10 derniers jours

// we set default dates, today is 2017-05-29

$endDate = new DateTime();
$startDate = new DateTime();

// substract 10 days with the object DateInterval
$startDate->sub(new DateInterval('P10D'));
// or you can use Date Interval with regular string

$startDate->sub(new DateInterval::createFromDateString('10 Days'));
// or you can use the construct if you don't like DateInterval

$startDate = new DateTime("now - 10 days");

// startDate = 2017-05-19 , endDate = 2017-05-29

Demande : extraire la dernière demi-semaine, soit du lundi au mercredi, ou du jeudi au dimanche

// If today is between Monday to Wednesday,
// We want to get dates from last Thursday to last Sunday
$today = new DateTime();

// 'N' indicate the day number, Monday = 1, Tuesday = 2 ... Sunday = 7
if ($today->format('N') < 4 ){
    $startDate = new DateTime('Last Thursday');
    $endDate = new DateTime('Last Sunday');
}

// else, today is between Thursday to Sunday,
// we want to get dates  from Last Monday to Last Wednesday
else {
    $startDate = new DateTime('Last Monday');
    $endDate = new DateTime('Last Wednesday');
}

Demande: extraire la dernière semaine, mais si la semaine est à cheval entre 2 mois, il faut prendre le 1er jour du mois

// We need to check If the Last Monday is in the last month

$lastMondayDate = new DateTime('Last Monday');
$lastDayOfLastMonthDate = new DateTime('Last Day of Last Month');

// if the last monday is indeed in the past month,
// we take the first day of this month
if ($lastMondayDate->format('m') === $lastDayOfLastMonthDate->format('m')){
    $startDate = new DateTime('First Day of This Month');
}
// else we take the last monday
else{
    $startDate = $lastMondayDate ;
}

// in both cases, end date is the Last Sunday
$endDate = new DateTime('Last Sunday');

Que se passe-t-il si je fais ceci ??

// Today is 2017-05-29

$date = new DateTime("first day of last month");

echo $date->format('Y-m-d') , ' => ';

$date->setDate(2013, 2, 3);

echo $date->format('Y-m-d');

Executons ça sur 3v4l.org

computer cat
// Today is : 2017-05-29

$date = new DateTime("first day of last month");

echo $date->format('Y-m-d') , ' => ';

$date->setDate(2013, 2, 3);

echo $date->format('Y-m-d');

// in PHP 7.0.17 - 7.0.19, 7.1.3 - 7.2.0rc2

// 2017-04-01 => 2013-02-03

// in PHP 5.6.0 - 5.6.30, 7.0.0 - 7.0.16, 7.1.0 - 7.1.2

// 2017-04-01 => 2013-02-01
nope

Whoa, c'est fort problématique !

  • Le comportement n'est pas le même en fonction de la version de PHP
  • Le problème est le même avec certaines autres phrases
// Anoter Exemple, we create with Last day of this month

$date = new DateTime("last day of this month");
echo $date->format('Y-m-d') , ' => ';

$date->setDate(2012, 2, 03);
echo $date->format('Y-m-d');

// in PHP 7.0.17 - 7.0.19, 7.1.3 - 7.2.0rc2

// 2017-05-31 => 2012-02-03

// in PHP 5.6.0 - 5.6.30, 7.0.0 - 7.0.16, 7.1.0 - 7.1.2

// 2017-05-31 => 2012-02-29

Finalement il a calculé le dernier jour de Février 2012 (année bissextile)

Mon conseil: Ne le faites pas !
En tout cas pas avant un bon moment !

Si vous travaillez sur une version inférieure à 7.1.3, cela risque de ne pas marcher...

Conclusion

Avec les bonnes pratiques d'aujourd'hui

  • DateTimeImmutable avec DateTimeZone sont les plus utilisés
  • Les fonctions date() avec des timestamps étaient surtout utilisées en procédural

Attention !!

  • Ne créez jamais de dates avec une chaine de caractères sans les avoir testées dans plusieurs situations !
    Par exemple: au 31 Janvier, sur une année Bissextile, etc...
  • Gardez en tête qu'un comportement sur une version de PHP ne marchera pas forcément sur une autre
  • faketime : modifie la date du système pour l'application

Pour simplifier...

One does not simply trust a DateTime function

Des questions ??

You get a question! You get a question! Everyone gets a question!

Merci ;)

My work here is done, thank you all