Suggesting Carbon with Composer – Date and Time the Right Way

Bruno Skvorc
Share

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.

Date and time image

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!