Handling dates with PHP


I’ve seen a lot of newbies (and not newbies) having problems handling dates in PHP (and even with SQL and another languages). When I see someone having problems with dates, I always ask the same question. I type in a text editor “27/11/2012” and I ask him: What is it? If your answer is “This is a date” you should continue reading the post 🙂

“27/11/2012” in a text editor is not a date. It’s a string representing a date (my birthday this year, indeed 🙂 ). We also must take into account that we cannot show a “raw” date. We only can apply a mask to a date if we want to show it. Because of that dates are normally stored internally as UNIX timestams or something similar.You can say that when we perform one SELECT statement with a date field we don’t see one timestamp. That’s because the tool that you are using to show the date field applies one mask to your date. The default mask can be also a problem. “01/02/2012” represents 01/February/2012 or 02/January/2012? We don’t know. Depends on the mask that we are using.

When we work with PHP is very usual to use dates. Sometimes we need to pass them to SQL queries, perform operations (such as add days, diffs, compare, …). When I started with PHP 10 years ago we use to use mktime(), date(), and insane operations with split/join with strings. That’s the past. Now we have DateTime.

It’s pretty straightforward to use it. We can use one static factory to create DateTime objects

$date = DateTime::createFromFormat('d/m/Y', '27/11/2012');

Now $date is a variable that stores a DateTime object. If we want to show it, we need to apply one format. For example:

$date = DateTime::createFromFormat('d/m/Y', '27/11/2012');
echo $date->format('m/d/Y');  // outputs 11/27/2012

I’m not going to create a tutorial of DateTime here. It’s very good described (as always) within PHP official documentation. There is something that I love with DateTime objects also:

$date1 = DateTime::createFromFormat('d/m/Y', '27/11/2012');
$date2 = DateTime::createFromFormat('d/m/Y', '27/11/2011');

if ($date1 > $date2) {
    echo "It works!"
}

In my last projects when I need to use Dates I like to cast the variables as DateTime objects. Let me show you a little example:

<?php

class Dummy
{
    /** @var DateTime */
    private $date;
    private $defaultFormat = 'd/m/Y';

    public function setDate(DateTime $date)
    {
        $this->date = $date;
    }

    public function getDate()
    {
        return $this->date;
    }

    public function getFormattedDate($format = NULL)
    {
        return $this->date->format($this->getFormat($format));
    }

    public function setFormattedDate($formatedDate, $format = NULL)
    {
        $this->date = DateTime::createFromFormat($this->getFormat($format), $formatedDate);
    }

    public function setDefaultFormat($format)
    {
        $this->defaultFormat = $format;
    }

    private function getFormat($format)
    {
        return is_null($format) ? $this->defaultFormat : $format;
    }
}

But maybe, as always, the best documentation are the Unit Tests

<?php

class DummyTest extends \PHPUnit_Framework_TestCase
{
    public function testDate()
    {
        $date  = DateTime::createFromFormat('d/m/Y', '1/2/2012');
        $dummy = new Dummy();
        $dummy->setDate($date);
        $this->assertEquals($date, $dummy->getDate());
    }

    public function testDateWithAlternativeSetter()
    {
        $dummy = new Dummy();
        $dummy->setFormattedDate('1/2/2012', 'd/m/Y');
        $this->assertEquals(DateTime::createFromFormat('d/m/Y', '1/2/2012'), $dummy->getDate());
    }

    public function testDateWithAlternativeSetterWithDeffaultFormatSet()
    {
        $dummy = new Dummy();
        $dummy->setDefaultFormat('d/m/Y');
        $dummy->setFormattedDate('1/2/2012');
        $this->assertEquals(DateTime::createFromFormat('d/m/Y', '1/2/2012'), $dummy->getDate());
    }

    public function testFormatGetterWithDefaultFormat()
    {
        $dummy = new Dummy();
        $dummy->setFormattedDate('1/2/2012');
        $this->assertEquals('2012', $dummy->getDate()->format('Y'));
    }
    
    public function testFormatGetter()
    {
        $dummy = new Dummy();
        $dummy->setFormattedDate('1/2/2012');
        $this->assertEquals('2012', $dummy->getFormattedDate('Y'));
    }

    public function testDiffDates()
    {
        $dummy = new Dummy();
        $dummy->setFormattedDate('1/2/2012');
        $date = DateTime::createFromFormat('d/m/Y', '1/2/2011');

        $this->assertTrue($date < $dummy->getDate());
    }

    public function testModifyDates()
    {
        // leap-years
        $dummy = new Dummy();
        $dummy->setFormattedDate('28/02/1908');
        $this->assertEquals('29/02/1908', $dummy->getDate()->modify("+1 day")->format('d/m/Y'));

        // Non leap-years
        $dummy = new Dummy();
        $dummy->setFormattedDate('28/02/2001');
        $this->assertEquals('01/03/2001', $dummy->getDate()->modify("+1 day")->format('d/m/Y'));
    }
}

What do you think?

10 thoughts on “Handling dates with PHP

    1. DateTime belongs to the core of the PHP language (it’s not one extra extension). Probably (not checked) if there’s any performance difference, it will be very small. IMHO the usage of DateTime objects create more readable and clean interfaces.

  1. The best way is to convert a string using strtotime and then convert to an actual date using the Date function. This is the fastest and most efficient ways, especially when comparing dates.

    1. I ‘ve benchmarked this statement and can not confirm that die procedural functions are faster than the DateTime object. I ‘ve testet this with PHP 5.4.7 an a local machine with xampp.

      procedural style:
      $timestamp = strtotime(‘01.01.2013’);
      $date = date(‘Y-m-d’, $timestamp);

      object notation style:
      $datetime = DateTime::createFromFormat(‘d.m.Y’, ‘01.01.2013’);
      $date = $datetime->format(‘Y-m-d’);

      I ‘ve repeatet both of them in a for-Loop for 10.000 times. The DateTime Object was 0.1 seconds faster on average. Comparing dates with both styles had a similar result.

  2. Note that DateTime currently does not have localization support, that means you have to use DateTime::getTimestamp() as a second parameter to strftime() to get localized date representations.

  3. Thanks for the post Gonzalo.

    How do you deal with invalid dates? DateTime::createFromFormat and DateTime::__construct won’t validate the dates, they’ll just allow non-valid dates to roll over.

    For example (taken from the PHP documentation):

    new DateTime(‘2000-02-30’)
    echo $date->format(‘Y-m-d’)

    Will output :

    2000-03-01

    The difference lies in how Exceptions/Errors are handled by each one.

    DateTime::__construct throws an Exception in case of an invalid date, and its procedural counterpart date_create() would return FALSE.

    But that’s not the case for DateTime::createFromFormat, which apparently doesn’t throw and Exception. Neither its procedural interface date_create_from_format() would return FALSE.

    I can’t understand the reasoning for that inconsistency. As far as I know, when using DateTime::createFromFormat you should check the Warning string generated by DateTime::getLastErrors, but no error will be generated.

    http://www.php.net/manual/en/datetime.getlasterrors.php#102686

    So, I’m still using checkdate() for its convenience. But I’d much prefer DateTime::createFromFormat to support proper date validation / error reporting, as that would be much more flexible than the fixed format of checkdate().

    What do you think? How do you deal with all those validation inconsistencies?

  4. I understand the problem. I’m in the same situation. We need checkdate or awful code with getlasterrors. Anyway we have the same problem with mktime.

    Let me meditate a bit about this problem. I think we can improve DateTime a little bit to solve this issue. Stay tuned to next posts.

    1. Just for the record, DateTime::setDate behaves the same as DateTime::createFromFormat. That is, not throwing an Exception, nor returning FALSE for the procedural date_date_set().

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.