Suggesting Carbon with Composer – Date and Time the Right Way
Carbon is a small library for date and time manipulation in PHP. It relies on and extends the core DateTime class, adding helpful methods for a significantly saner experience.
In this article, we’ll take a look at some basic usage examples, and then use it in a real project.
Introduction
Carbon is just a class which is designed to be used instead of DateTime. Due to extending DateTime, all DateTime methods are available to users of Carbon. Additionally, it implements a __toString
method, allowing users to put it in place of string representations of date and time as well.
It can easily be installed with Composer:
composer require nesbot/carbon
Let’s see some example uses, as presented in their excellent documentation.
Example Uses
The easiest way to get started with Carbon is to just pass a human readable date string into its constructor, along with an optional timezone – if the timezone is omitted, the one set by the current PHP installation will be used.
$carbon = new Carbon('first day of next week');
It can also be instantiated from strings, timestamps, even other instances of DateTime or even Carbon. The instance can be copied with the copy()
method, for efficient cloning.
From there, we have access to a smorgasbord of helper checkers and getters:
$carbon->isWeekend();
$carbon->isFuture();
$carbon->isLeapYear();
$carbon->year;
$carbon->month;
$carbon->daysInMonth;
$carbon->weekOfYear;
The package also exposes static methods for creating new instances quickly:
echo Carbon::now()->addYear()->diffForHumans(); // in 1 year
Even birthdays can be checked, as we can see by this example from the docs:
$born = Carbon::createFromDate(1987, 4, 23);
$noCake = Carbon::createFromDate(2014, 9, 26);
$yesCake = Carbon::createFromDate(2014, 4, 23);
$overTheHill = Carbon::now()->subYears(50);
var_dump($born->isBirthday($noCake)); // bool(false)
var_dump($born->isBirthday($yesCake)); // bool(true)
var_dump($overTheHill->isBirthday()); // bool(true) -> default compare it to today!
Localization
Localization is also supported, so that output can be given in any desired language installed on the machine powering the PHP app. Note that you do need to install the necessary locales for this to work – refer to your operating system’s documentation for details on how to do that.
To localize date and time strings, the standard PHP function setlocale
can be used:
setlocale(LC_TIME, 'German');
echo $dt->formatLocalized('%A %d %B %Y'); // Mittwoch 21 Mai 1975
To localize the diffForHumans
method which outputs a human-readable difference in time, the class offers its own setLocale
method:
Carbon::setLocale('de');
echo Carbon::now()->addYear()->diffForHumans(); // in 1 Jahr
Interval
A CarbonInterval class is also provided, which is an extension of DateInterval. Self-descriptively, it holds interval values, just like the base class, but adds helper methods on top. As per examples:
echo CarbonInterval::year(); // 1 year
echo CarbonInterval::months(3); // 3 months
echo CarbonInterval::days(3)->seconds(32); // 3 days 32 seconds
echo CarbonInterval::weeks(3); // 3 weeks
echo CarbonInterval::days(23); // 3 weeks 2 days
echo CarbonInterval::create(2, 0, 5, 1, 1, 2, 7); // 2 years 5 weeks 1 day 1 hour 2 minutes 7 seconds
Note that Carbon as a whole is exceptionally well documented – for a full reference of methods and usage examples, please see their docs.
Implementation
In this section, we’ll upgrade the Diffbot PHP Client to optionally support Carbon. The plan is as follows: if the user has the library installed, then the Article entity and Post entity will return Carbon instances instead of date strings from their getDate
and getEstimatedDate
methods. Otherwise, they’ll return strings as usual.
If you’d like to follow along, clone the client at this version.
Composer Suggests
The first step is to add the library to the suggests
list in composer.json
. The suggests
list takes the same format as the require
blocks, but instead of version constraints, we put full string messages on why that package is suggested.
"suggest": {
"nesbot/carbon": "Turns the date and estimatedDate return values of Article and Post entity into Carbon entities."
},
We can make sure we got the syntax right by running composer validate
:
vagrant@homestead:~/Code/diffbot-php-client$ composer validate
./composer.json is valid
When a user installs the Diffbot PHP Client, they’ll see a recommendation to install Carbon.
Tests
Next, it’s time to update the tests to accommodate for this.
In tests/Entities/ArticleTest.php
, we alter the dateProvider
and testDate
functions like so:
public function dateProvider()
{
return [
[
'Articles/diffbot-sitepoint-basic.json',
"Sun, 27 Jul 2014 00:00:00 GMT",
2014
],
[
'Articles/diffbot-sitepoint-extended.json',
"Sun, 27 Jul 2014 00:00:00 GMT",
2014
],
[
'Articles/apple-watch-verge-basic.json',
"Wed, 08 Apr 2015 00:00:00 GMT",
2015
],
[
'Articles/apple-watch-verge-extended.json',
"Wed, 08 Apr 2015 00:00:00 GMT",
2015
]
];
}
/**
* @param $file
* @param $articles
* @dataProvider dateProvider
*/
public function testDate($file, $articles, $year)
{
$articles = (is_array($articles)) ? $articles : [$articles];
/** @var Article $entity */
foreach ($this->ei($file) as $i => $entity) {
$this->assertEquals($articles[$i], $entity->getDate());
if (class_exists('\Carbon\Carbon')) {
$this->assertEquals($year, $entity->getDate()->year);
}
}
}
What happened here?
First, we added year values into the provider as the third argument to be passed into testDate
. Then, during iteration and assertion, we first check if the class is loaded / exists, and if so, we test for one of its getters (->year
).
We have to check if the class exists before testing, because Carbon is optional in the Diffbot SDK – it’s just a suggestion, so we mustn’t fail if it isn’t there.
We repeat the process for estimatedDateProvider
and estimatedDate
at the bottom of the ArticleTest
class:
public function estimatedDateProvider()
{
return [
['Articles/15-11-07/diffbot-sitepoint-basic.json', 'Sun, 27 Jul 2014 00:00:00 GMT', 2014],
];
}
/**
* @dataProvider estimatedDateProvider
* @param $file
* @param $value1
*/
public function testEstimatedDate($file, $value1, $value2)
{
$value1 = (is_array($value1)) ? $value1 : [$value1];
/** @var Article $entity */
foreach ($this->ei($file) as $i => $entity) {
$this->assertEquals($value1[$i], $entity->getEstimatedDate());
if (class_exists('\Carbon\Carbon')) {
$this->assertEquals($value2, $entity->getDate()->year);
}
}
}
Next, let’s update the PostTest
class. That one only has the getDate
method, but involves a bit more typing because a Discussion almost always returns multiple posts.
{
return [
[
'Discussions/15-05-01/sp_discourse_php7_recap.json',
[
"Wed, 29 Apr 2015 16:00:00 GMT",
"Thu, 30 Apr 2015 01:13:00 GMT",
"Thu, 30 Apr 2015 05:55:00 GMT",
"Thu, 30 Apr 2015 06:57:00 GMT",
"Thu, 30 Apr 2015 07:51:00 GMT",
"Thu, 30 Apr 2015 09:29:00 GMT",
"Thu, 30 Apr 2015 10:26:00 GMT",
"Thu, 30 Apr 2015 10:40:00 GMT",
"Thu, 30 Apr 2015 11:06:00 GMT",
"Thu, 30 Apr 2015 11:29:00 GMT",
"Thu, 30 Apr 2015 14:33:00 GMT",
"Thu, 30 Apr 2015 15:48:00 GMT",
"Thu, 30 Apr 2015 16:17:00 GMT",
"Thu, 30 Apr 2015 16:51:00 GMT",
"Thu, 30 Apr 2015 17:02:00 GMT",
"Fri, 01 May 2015 08:00:00 GMT",
],
[
2015,
2015,
2015,
2015,
2015,
2015,
2015,
2015,
2015,
2015,
2015,
2015,
2015,
2015,
2015,
2015,
]
]
];
}
/**
* @param $file
* @param $posts
* @dataProvider dateProvider
*/
public function testDate($file, $posts, $years)
{
/** @var Discussion $entity */
foreach ($this->ei($file) as $entity) {
/** @var Post $post */
foreach ($entity->getPosts() as $i => $post) {
$this->assertEquals($posts[$i], $post->getDate());
if (class_exists('\Carbon\Carbon')) {
$this->assertEquals($years[$i], $post->getDate()->year);
}
}
}
}
If we now attempt to run these tests, they will succeed. If we install Carbon into the project with:
composer require nesbot/carbon --dev
… the tests will fail.
Implementation
It’s time to change the actual entities now.
Diffbot returns dates in the following format: Sun, 27 Jul 2014 00:00:00 GMT
.
In order to maintain backwards compatibility, we need to set Carbon to produce the same output when used as a string (via __toString
), so that everyone who used the date values as output directly can still do so. This is done with the static Carbon::setToStringFormat($format);
method.
Adding the following to the constructor in src/Entity/Article.php
will accomplish this:
if (class_exists('\Carbon\Carbon')) {
$format = 'D, d M o H:i:s e';
\Carbon\Carbon::setToStringFormat($format);
}
The same must be added to the src/Entity/Post.php
file, though that one still doesn’t override the base class’ constructor and needs to do that first. The final version of Post
‘s construct method is:
public function __construct(array $data)
{
if (class_exists('\Carbon\Carbon')) {
$format = 'D, d M o H:i:s e';
\Carbon\Carbon::setToStringFormat($format);
}
parent::__construct($data);
}
Now that we’ve got Carbon activated and defaulting to a format we want, it’s time to upgrade our getters.
In both Article
and Post
, the getDate
method should now look like this:
public function getDate()
{
return (class_exists('\Carbon\Carbon')) ?
new \Carbon\Carbon($this->data['date'], 'GMT') :
$this->data['date'];
}
If Carbon exists, make a new instance from the post’s date in the GMT timezone (the timezone Diffbot always returns), otherwise, return the date string as before.
Finally, we need to change getEstimatedDate
in Article
:
public function getEstimatedDate()
{
$date = $this->getOrDefault('estimatedDate', $this->getDate());
return (class_exists('\Carbon\Carbon')) ?
new \Carbon\Carbon($date, 'GMT') :
$date;
}
Same thing, only this one first defaults to getDate
if the estimatedDate
could not be determined.
Running the tests should now show everything passing:
vagrant@homestead:~/Code/diffbot-php-client$ phpunit
PHPUnit 5.0.8 by Sebastian Bergmann and contributors.
Runtime: PHP 5.6.10-1+deb.sury.org~trusty+1 with Xdebug 2.3.2
Configuration: /home/vagrant/Code/diffbot-php-client/phpunit.xml.dist
............................................................... 63 / 352 ( 17%)
............................................................... 126 / 352 ( 35%)
............S.................................................. 189 / 352 ( 53%)
............................................................... 252 / 352 ( 71%)
............................................................... 315 / 352 ( 89%)
..................................... 352 / 352 (100%)
Time: 49.39 seconds, Memory: 21.00Mb
Success! We can now commit, push, and publish a new release!
Conclusion
We looked at Carbon, an extension of DateTime which adds helpful methods to the core class and make it much more pleasant to use. We saw how easy it is to implement in a project, and how it can replace pure string outputs and timestamps by means of different internal string formats.
Are you using Carbon in your projects? What do you like or dislike about it? Leave your thoughts and comments below, and if you liked this post, don’t forget to hit that thumbs up button!