whitewashing.de

Immutable DateTime Objects

08. January 2010

One serious drawback of PHPs DateTime extension is the mutability of its instances. It can lead to serious bugs if a DateTime instance is passed between different functions and is modified at unexpected places. Additionally this possibly rules out several optimizations for scripts that make very heavy use of dates and could share references to equal timestamps.

Warning: All the code assumes that you work with one timezone only!

The following code is an immutable version of PHP's DateTime. All setter methods throw an exception and add(),sub() and modify() clone the current instance, apply the operation and return the new instance.

namespace Whitewashing\DateTime;

class DateTime extends \DateTime
{
    /**
     * To prevent infinite recursions
     *
     * @var bool
     */
    private static $_immutable = true;

    public function add($interval)
    {
        if (self::$_immutable) {
            self::$_immutable = false;
            $newDate = clone $this;
            $newDate->add($interval);
            self::$_immutable = true;
            return $newDate;
        } else {
            return parent::add($interval);
        }
    }

    public function modify($modify)
    {
        if (self::$_immutable) {
            self::$_immutable = false;
            $newDate = clone $this;
            $newDate->modify($modify);
            self::$_immutable = true;
            return $newDate;
        } else {
            return parent::modify($modify);
        }
    }

    public function sub($interval)
    {
        if (self::$_immutable) {
            self::$_immutable = false;
            $newDate = clone $this;
            $newDate->sub($interval);
            self::$_immutable = true;
            return $newDate;
        } else {
            return parent::sub($interval);
        }
    }

    public function setDate($year, $month, $day) {
        throw new ImmutableException();
    }
    public function setISODate($year, $week, $day=null) {
        throw new ImmutableException();
    }
    public function setTime($hour, $minute, $second=null) {
        throw new ImmutableException();
    }
    public function setTimestamp($timestamp) {
        throw new ImmutableException();
    }
    public function setTimezone($timezone) {
        throw new ImmutableException();
    }
}
class ImmutableException extends \Exception
{
    public function __construct()
    {
        parent::__construct("Cannot modify Whitewashing\DateTime\DateTime instance, its immutable!");
    }
}

Its not the prettiest code, but it works.

A next optimization would be a DateFactory that manages DateTime instances by returning already existing instances for specific dates. This is not a perfect solution, since you won't be able to enforce a single instance when you are using the relative descriptive dates or when calculating with dates using add(), sub() and modify(), however for lots of dates created from a database or other external source it might be quite a powerful optimization depending on your use-case:

namespace Whitewashing\DateTime;

class DateFactory
{
    static private $_dates = array();

    static public function create($hour, $minute, $second, $month, $day, $year)
    {
        $ts = mktime($hour, $minute, $second, $month, $day, $year);
        if (!isset(self::$_dates[$ts])) {
            self::$_dates[$ts] = new DateTime('@'.$ts);
        }
        return self::$_dates[$ts];
    }

    static public function createFromMysqlDate($mysqlDate)
    {
        list($date, $time) = explode(" ", $mysqlDate);
        if($time == null) {
            $hour = $minute = $second = 0;
        } else {
            list($hour, $minute, $second) = explode(":", $time);
        }
        list($year, $month, $day) = explode("-", $mysqlDate);
        return self::create($hour, $minute, $second, $month, $day, $year);
    }
}

This includes some date time calculations and date creation with mktime() and DateTimes unix timestamp capabilities to be able to work. Otherwise the sharing of instances could not be implemented. If you need to create shareable instances from other formats you could just create another creation method for it and convert the format for create() to be used.

:: Category: PHP, Comments (4)

Comments (4)

I had the same need just a few weeks before. I like this approach better because it extends on DateTime. My own class addresses a specific usecase and just encapsulates DT objects, which leads to a lot of cloning whenever you get a date from it. User: john - Date: 2010/01/08 11:25
I've got a more generic constant references implementation in native PHP which solves this quite nicely. Just pass const_ref($dtObj) into a function and you're safe. It works basically the same way as the first example by wrapping up the object and intercepting property and method accesses. User: will - Date: 2010/01/08 12:56
I think it is the repsonsibility of the creator of the code to document what a specific function does with it's parameters. If you know that your DateTime instance will be altered in a function (it could be that this is on purpose) and you want to preserve your original object, you can clone the DateTime instance before calling the given function.

It is a feature that objects are passed by reference. User: joris - Date: 2010/01/09 16:38
I think it is the repsonsibility of the creator of the code to document what a specific function does with it's parameters. If you know that your DateTime instance will be altered in a function (it could be that this is on purpose) and you want to preserve your original object, you can clone the DateTime instance before calling the given function.

It is a feature that objects are passed by reference. User: joris van de sande - Date: 2010/01/09 16:38

Post A Comment

 
 
 
 
::Back To The Top