PHP and Dates

Seriously??

A slide desk on Dates??

  • Yes, dates are tricky!
  • Many ways to generate dates, lots of them getting you in trouble...

We will see...

  1. Date function (obviously)
  2. Timestamp functions: mktime, strtotime
  3. DateTime Object
  4. DateTimeImmutable Object
  5. DateTimeZone Object
  6. DateInterval Object

date function

Let's start with basic

  • It's the first function we though about, obviously
  • It was for me to generate dates for accounting request
  • php.net: function 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



Dates and timestamp

date() accepts a timestamp as parameter:

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

Timestamp function

Timestamp ???

A timestamp is the number of seconds since the 1st January 1970 

No Time Zone are taken with timestamp

mktime

// the mktime function take 6 parameters 

// BE CAREFUL WITH THE PARAMETERS ORDER

$hour = 0 ;

$minute = 0 ;

$second = 0 ;

$month = 10 ;

$day = 24 ;

$year = 2016 ;


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

$testDate = date('Y-m-d', $timestampToApply);

echo $testDate; // 2016-10-24

mktime look kind of crappy...

  • Yeah you're right, it can be very complicated
  • But it can give you chirurgical precisions if needed

"How can it be complicated"
you may ask?

Please ask...

// get the last day of last month, today is 2017-05-26

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

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

// the timestamp is on last month, I will get the last day, so number 30
$lastDayOfTheMonth = date('t', $timestampFirstDayOfLastMonth);

$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

// get the last day of last month, today is 2017-05-26
// 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')
    )
);


echo $lastDayOfLastMonth; // 2017-04-30

Behaviour to know

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

// 2017-02-02

I will try to put the date 33 January 2017

It will set to the date according to 31 January + 2 days

To conclude

  • You can use them but with care
  • Always test the mktime result
  • Comment what it does for future developers
  • php.net : function mktime

strtotime

// Today is Tuesday 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

So strtotime is good?

  • Yeah, can be great :)
  • Be careful at the "magic" inside the function
  • Some date cannot be made with strtotime
// WARNING !! first Specific day of last week will not give you what you want

echo date('Y-m-d'); // "2017-05-26"

$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 phrases seems Ok but does not work at all

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

// => 1970-01-01


// How do you construct the 15th of last month ?? you cannot with strtotime

$dayFifteenOfThisMonth = date('Y-m-d', mktime(0, 0, 0, date('m'), 15, date('Y')));

// => 2017-05-15

DateTime Object

Finally we are talking!!

I can forget previous chapters

Hum... No !!

Because behaviours are the same

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

$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
);

$dayFifteenOfThisMonth->format('Y-m-d');

// => 2017-05-15

And traps are the same!

// trap on the First Specific day of last Week
// Today is 2017-05-26

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

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

// => 2017-04-24


// Trap on phrases that seems normal but does not work at all

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

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

// Fatal error: Uncaught Exception: DateTime::__construct(): 
// Failed to parse time string (Monday of Last Week) 

 DateTimeImmutable Object

It's the same as using DateTime

The only difference is the DateTimeImmutable cannot be altered once created.

If you do, it will create a new DateTime Object

One useful function:

createFromMutable

// create date at day 15th of this month

$date = new \DateTime();

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

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

$dayFifteenOfThisMonth = \DateTimeImmutable::createFromMutable($date);

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

// 2017-05-15

DateTimeZone Object

Always be cautious with the current TimeZone

  • By default set your timezone to "UTC"
  • Be reminded to change it when you work with international dates
  • Check also the server Timezone you are working on
// 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:sP') . "\n";

$date->setTimezone(new DateTimeZone('Australia/Sydney'));
echo "Australia/Sydney " . $date->format('Y-m-d H:i:sP') . "\n";

// Europe/Paris 2017-05-01 00:00:00+02:00
// Australia/Sydney 2017-05-01 08:00:00+10:00

Available Timezone 

// 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') . "\n";

// 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') . "\n";

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

And of course... some traps

// 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

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

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

// bool(false)

To avoid this trap

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

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

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

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

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

DateInterval Object

Allow you to set a period

  • Only two functions, and they are quite similar
  • Using DateTime->diff will give you a DateInterval

How I read the construct ?

  • The "P" is for "Period", you set years to days
  • The "T" is for "Time", you set hours to second
  • In the exemple below :
    P: 2Year 4Day
    T: 6Hour 8Minute
// set a date Interval

$dateInterval = new DateInterval('P2Y4DT6H8M');

// equivalent to

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

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

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

Some real situations

Request : Get the last half week, meaning from Monday to Wednesday or from Thursday to Sunday

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

// format N indicate the day number, Monday = 1, Tuesday = 2, etc...
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');
}

Request : Get the last week, but if the week is between two months, take from the 1st day of this month

// 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
// else we take the last monday

if ($lastMondayDate->format('m') === $lastDayOfLastMonthDate->format('m')){
    $startDate = new DateTime('First Day of This Month');
}
else{
    $startDate = $lastMondayDate ;
}

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

Request : Get dates for a rolling 10 days period

// 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 setDate if you don't like DateInterval

$startDate->setDate(
    (int) $startDate->format('Y'),
    (int) $startDate->format('m'),
    (int) $startDate->format('d')-10
);

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

What will happen if you do this ??

// 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');

Let's try that on 3v4l.org

// 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');


// for 7.0.17 - 7.0.19, 7.1.3 - 7.2.0rc2

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

// for 5.6.0 - 5.6.30, hhvm-3.15.4 - 3.19.0, 7.0.0 - 7.0.16, 7.1.0 - 7.1.2

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

This is tricky!

  • It's not the same behaviour depending of the PHP version

  • It's the same problem with some others String, exemple:

// Anoter Exemple, we create with Last day of this month
// and we set the new date on a Leap Year

$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');


// for 7.0.17 - 7.0.19, 7.1.3 - 7.1.5

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

// for 5.6.0 - 5.6.30, hhvm-3.15.4 - 3.19.0, 7.0.0 - 7.0.16, 7.1.0 - 7.1.2

// 2017-05-31 => 2012-02-29 : it take in fact the last day of February 2012

My advise: don't do it!

For a while at least!

If you are working on a PHP version under 7.1.3, it might not work...

Conclusion

With good practices of today

  • DateTimeImmutable with DateTimeInterval are the most used
  • Date function and timestamp functions were used with procedural way of coding

Watch out! 

  • Never create date from string without test it on several situations
  • Exemple : on 31 January, on leap Year, etc...
  • Keep in mind that a behaviour in one php version might not work on another

Merci ;)

PHP and Dates

By Kevin JHappy

PHP and Dates

First version for the Forum PHP 2017

  • 341