In PHP most know of DateTime
class to handle date and time.
At least with more modern PHP versions it is now not advised anymore to use the plain old date()
and time()
functions.
The use cases – especially with a more global world – these days more often include correct time zone handling as well as more robust
delta handling. But using objects also means you have to be more careful about (accidentally) modifying the original date when you are creating a new one from it.
Until now the de-facto standard pretty much was Carbon – as we all know.
It wrapped the DateTime
object and applied some necessary bugfixes as well as a lot of useful enhancements like better object oriented access for reading and writing.
We at CakePHP also wanted to start using it, faced quite a few issues though at the time.
One was, that it seemed unmaintained over months of time – often times with critical or at least major bugs not being fixed.
There was also the problem that there was no (and still not is) fork or version for a more modern PHP 5.4+ approach. We actually wanted and needed PHP 5.5+ support due to a lot of necessary enhancements of date time handling in PHP, more to that later.
So even after approaching them multiple times, trying to offer a helping hand here, not much changed.
The solution for us then was the only viable one on the table: We need to create a clone of it, and start maintaining it ourselves.
Chronos as modern and future proof stand-alone library to handle date and (date)time.
As a side-effect we were able to also implement better interfaces around it and could leverage all the new PHP features.
And as of this week, the Chronos library is officially marked as stable 1.0.0
๐
Main differences and improvements
- Implements ChronosInterface for proper typehinting, e.g. in methods
- DateTime and Date (no time) handling separately per use case
- Immutable by default for cleaner coding and less errors
- Correct some faulty standards (ISO code violations) and behavior (difference calculation)
- No external dependencies
Mutable vs Immutable
That topic is present throughout many layers of data handling, but with objects being passed around by reference, this is especially important around
data manipulation inside your business layer.
Using mutable by default means that you could easily modify a DateTime object (or Chronos in this case maybe) by accident.
You could have added a day to check if that following one is still a weekday, but at the same time this modification than accidentally back out of the method and down the chain of method invocations. The next method then uses the altered datetime and so on and so on.
// Bad practice - and doesn't work with immutable objects
$datetime->addDay(1);
$this->doSomething($datetime);
return $datetime;
// Better to never touch the original object - this works like you'd expect
$datetime = $datetime->addDay(1);
$datetime = $this->doSomething($datetime);
return $datetime;
Shimming buggy PHP core behavior
Intuitively, if you add months instead of specific days to a date, you would expect this to be "month-exact", not "day-exact".
$dt = new DateTime('2015-01-31');
$dt->modify("+1 month");
echo $dt->format("Y-m-d H:i:s"); //2015-03-03 00:00:00
Clearly, this overflows in unexpected ways.
So Chronos actually gives you addMonths()
/subMonths()
that actually work as desired:
$dt = new Chronos('2015-01-31');
$dt = $dt->addMonths(1);
echo $dt->format("Y-m-d H:i:s"); //2015-02-28 00:00:00
To get the former PHP behavior back, you can explicitly use addMonthsWithOverflow()
/subMonthsWithOverflow()
methods. Not that is is ever useful or recommended ๐
Testing and fixating time
Everyone knows those one second issues when writing tests and (date)time. Sometimes tests fail because the time for now()
jumped to the next second.
When writing unit tests, it is helpful to fixate the current time. Chronos lets you fix the current time for each class. As part of your test suiteโs bootstrap process you can include the following:
Chronos::setTestNow(Chronos::now());
MutableDateTime::setTestNow(MutableDateTime::now());
Date::setTestNow(Date::now());
MutableDate::setTestNow(MutableDate::now());
This will fix the current time of all objects to be the point at which the test suite started.
Usage
The Chronos API offers a very fast and intuitive way to work with datetime.
Let’s say you want to find the next Tuesday, if the current one is not already one:
$dt = new Chronos('2015-01-31');
if (!$dt->isTuesday()) {
$dt = $dt->next(ChronosInterface::TUESDAY);
}
Quite convenient are also the checks to find out whether a date is in the past or the future:
$dt->isPast();
$dt->isFuture();
Of course, you could also use a more verbose way with gt()
/lt()
and a current "now" datetime.
Check out the official chronos docs for how to use it in general.
Usage in frameworks
Usually, frameworks should be able to switch inside DB layer from DateTime or Carbon to Chronos easily.
In CakePHP for example the type conversation is setup in the bootstrap, and it already uses the immutable Chronos objects by default:
// bootstrap.php
/**
* Enable immutable time objects in the ORM.
*
* You can enable default locale format parsing by adding calls
* to `useLocaleParser()`. This enables the automatic conversion of
* locale specific date formats. For details see
* @link http://book.cakephp.org/3.0/en/core-libraries/internationalization-and-localization.html#parsing-localized-datetime-data
*/
Type::build('time')
->useImmutable();
Type::build('date')
->useImmutable();
Type::build('datetime')
->useImmutable();
The underlying classes here extend Carbon and so in the ORM all ingoing and outgoing datetimes are Chronos objects.