Asial Blog

Recruit! Asialで一緒に働きませんか?

PHPで日付時刻の処理を書くなら Carbon がおすすめ

カテゴリ :
バックエンド(プログラミング)
タグ :
Tech
PHP
Carbon
DateTime


どうも、筋トレにハマっているたきゃはしです。
本日はPHPにおける日付時刻のライブラリについて書いていきたいと思います。

突然ですが、日付や時刻が絡む処理って意外とやっかいだと思いませんか?おそらく皆さんもいくつか思い当たるフシがあるかと思いますが、そんなやっかい事も Carbon(カーボン)を使えば解消できるかもしれません!

Carbon - A simple PHP API extension for DateTime.

Carbon とはPHPのDateTimeクラスを継承して拡張された日時操作ライブラリです。

「Carbonってどうなの?流行ってるの?」という方向けに予め補足致します。
Carbonはすでに人気のフレームワークに統合されていたり、GitHubで☆3000に迫る評価もありますのでDateTimeライブラリとしてデファクトになるんじゃないかと予想できます。

ではインストールからはじめます。

  1. composer require nesbot/carbon
Composerでインストールすれば

  1. <?php
  2. require 'vendor/autoload.php';
  3.  
  4. use Carbon\Carbon;
  5.  
  6. echo Carbon::now(); // 2015-05-13 10:14:28(現在の日時)
  7.  
  8. Carbon::setTestNow(Carbon::create(2015, 5, 9, 15, 0, 0));
  9. echo Carbon::now(); // 2015-05-09 15:00:00
  10. echo Carbon::today()->toDateString(); // 2015-05-09
  11.  
  12. Carbon::setTestNow();
  13. $carbon = Carbon::create(1980, 4, 15, 10, 20, 30);
  14. echo $carbon->year;        // 1980
  15. echo $carbon->age;         // 35
  16.  
  17. echo Carbon::today()->subYears(13)->addMonths(2)->subDays(10)->toDateString(); // 2002-07-02
Carbonはすぐに使えます。

基本的にCarbonのメソッドはインスタンスを返します。なのでメソッドチェーンによる直感的な表現が可能です。またインスタンスを文字列型へとキャストするとデフォルトフォーマットに変換してくれます。

もちろんフォーマットを指定することも可能です。

  1. <?php
  2. require 'vendor/autoload.php';
  3.  
  4. use Carbon\Carbon;
  5.  
  6. $carbon = Carbon::create(1980, 4, 15, 10, 20, 30);
  7. echo $carbon; // 1980-04-15 10:20:30
  8. echo $carbon->format('Y年m月d日'); // 1980年04月15日
  9.  
  10. $carbon->setToStringFormat('Y/m/d H:i:s'); // デフォルトフォーマットを変更
  11. echo $carbon; // 1980/04/15 10:20:30
  12.  
  13. $carbon->resetToStringFormat();
  14. echo $carbon; // 1980-04-15 10:20:30

フォーマットの書式は date() と同じです。
- http://php.net/manual/ja/function.date.php

またCarbonではインスタンスを生成するための方法がいくつか準備されています。

  1. <?php
  2.  
  3. use Carbon\Carbon;
  4.  
  5. class CarbonTest extends PHPUnit_Framework_TestCase
  6. {
  7.  
  8.     /**
  9.      * @test
  10.      */
  11.     public function instance()
  12.     {
  13.         $dt = new \DateTime();
  14.         $carbon = Carbon::instance($dt);
  15.         $carbon2 = Carbon::instance($dt);
  16.         $this->assertTrue($carbon2 instanceof Carbon);
  17.         $this->assertTrue($carbon !== $carbon2);
  18.     }
  19.  
  20.     /**
  21.      * @test
  22.      */
  23.     public function copy()
  24.     {
  25.         $carbon = new Carbon();
  26.         $copiedCarbon = $carbon->copy();
  27.         $this->assertTrue($copiedCarbon instanceof Carbon);
  28.         $this->assertTrue($copiedCarbon !== $carbon);
  29.     }
  30.  
  31.     /**
  32.      * @test
  33.      */
  34.     public function parse()
  35.     {
  36.         $carbon = Carbon::parse('2002-07-02');
  37.         $this->assertSame(2002, $carbon->year);
  38.         $this->assertSame(7, $carbon->month);
  39.         $this->assertSame(2, $carbon->day);
  40.     }
  41.  
  42.     /**
  43.      * @test
  44.      */
  45.     public function create()
  46.     {
  47.         $carbon = Carbon::create(2002, 7, 2);
  48.         $this->assertSame(2002, $carbon->year);
  49.         $this->assertSame(7, $carbon->month);
  50.         $this->assertSame(2, $carbon->day);
  51.     }
  52.  
  53. }

基本的にcreate()もしくはparse()を使って生成することになりそうです。
parse()なんかはDBのDATE型やDATETIME型の値をそのまま引数に使えるので便利そうです。

次は Carbon::setTestNow() についてです。
setTestNow() はCarbonが基準とする現在の日時をモックで設定できる機能です。つまりロジック内でCarbonを適切に扱うことで時間に関するテストをユニットテストでも書けるようになります。

ユニットテストの例を用意しました。
賞味期限(BestBefore)が過ぎていないか確認するだけのシンプルな機能です。

  1. <?php
  2.  
  3. namespace Services;
  4.  
  5. use DateTime;
  6.  
  7. class BestBefore
  8. {
  9.  
  10.     protected $dt;
  11.  
  12.     public function __construct(DateTime $dt)
  13.     {
  14.         $this->dt = $dt;
  15.     }
  16.  
  17.     public function isSafe(DateTime $dt)
  18.     {
  19.         return $this->dt < $dt;
  20.     }
  21.  
  22. }

次に BestBefore::isSafe() のテストを書いてみます。

  1. <?php
  2.  
  3. use Carbon\Carbon;
  4. use Services\BestBefore;
  5.  
  6. class BestBeforeTest extends PHPUnit_Framework_TestCase
  7. {
  8.  
  9.     /**
  10.      * @test
  11.      */
  12.     public function 賞味期限が切れていないかどうか()
  13.     {
  14.         Carbon::setTestNow(Carbon::createFromDate(2015, 5, 1));
  15.         $expire = new BestBefore(new Carbon());
  16.  
  17.         $this->assertFalse($expire->isSafe(Carbon::createFromDate(2015, 4, 30)));
  18.         $this->assertFalse($expire->isSafe(Carbon::createFromDate(2015, 5, 1)));
  19.         $this->assertTrue($expire->isSafe(Carbon::createFromDate(2015, 5, 2)));
  20.  
  21.         Carbon::setTestNow(Carbon::createFromDate(2015, 4, 30));
  22.         $expire2 = new BestBefore(new Carbon());
  23.  
  24.         $this->assertFalse($expire2->isSafe(Carbon::createFromDate(2015, 4, 30)));
  25.         $this->assertTrue($expire2->isSafe(Carbon::createFromDate(2015, 5, 1)));
  26.         $this->assertTrue($expire2->isSafe(Carbon::createFromDate(2015, 5, 2)));
  27.     }
  28.  
  29. }

ご覧の通り Carbon::setTestNow() を設定することで冪等なテストを書くことができました。もしBestBeforeクラスのロジックでPHP標準の date() や time() を使っていたならこのようなテストは書けないですよね。個人的にすごく魅力的な仕組みだと感じました。

また例のBestBeforeクラスではタイプヒンティングにDateTimeを指定していますので、Carbonを拡張したMyCarbonを使いたいといった場合でもBestBeforeクラスに手を加えることなく利用することができます。

Carbonは単に「DateTime扱いやすいぞーやったー」というだけでなく、日時処理まわりのメンテナンス性およびコード品質の向上に一役買うライブラリであるということをお伝えできたかと思います。

ということで概ね私が伝えたかったのはここまでです。
ここからは覚えておくと便利なことや調べた機能をメモとして記載します。

アクセサ


  1. <?php
  2. require 'vendor/autoload.php';
  3.  
  4. use Carbon\Carbon;
  5.  
  6. $dt = Carbon::parse('2012-9-5 23:26:11.123789');
  7.  
  8. var_dump($dt->year);                                         // int(2012)
  9. var_dump($dt->month);                                        // int(9)
  10. var_dump($dt->day);                                          // int(5)
  11. var_dump($dt->hour);                                         // int(23)
  12. var_dump($dt->minute);                                       // int(26)
  13. var_dump($dt->second);                                       // int(11)
  14. var_dump($dt->micro);                                        // int(123789)
  15. var_dump($dt->dayOfWeek);                                    // int(3)          曜日。数値。0 (日曜)から 6 (土曜)
  16. var_dump($dt->dayOfYear);                                    // int(248)        年間の通算日。数字。(ゼロから開始)
  17. var_dump($dt->weekOfMonth);                                  // int(1)          月間の週番号。
  18. var_dump($dt->weekOfYear);                                   // int(36)         ISO-8601に基づく月曜日に始まる年単位の週番号。
  19. var_dump($dt->daysInMonth);                                  // int(30)         指定した月の日数。28 から 31。
  20. var_dump($dt->timestamp);                                    // int(1346901971)
  21. var_dump(Carbon::createFromDate(1975, 5, 21)->age);          // int(39)
  22. var_dump($dt->quarter);                                      // int(3)          四半期。

小ネタですが、上記プロパティはメンバとして定義されておらず __get() で動的に処理されています。age等は計算が必要だからですね。またIDEでプロパティが補完されたのですがどうやらDocで宣言すれば出来るようです。なるほど〜

- https://github.com/briannesbitt/Carbon/blob/master/src/Carbon/Carbon.php#L42

比較


  1. <?php
  2. require 'vendor/autoload.php';
  3.  
  4. use Carbon\Carbon;
  5.  
  6. $first = Carbon::create(2012, 9, 5, 23, 26, 11);
  7. $second = Carbon::create(2012, 9, 5, 7, 26, 11, 'America/Vancouver');
  8.  
  9. echo $first->toDateTimeString();                   // 2012-09-05 23:26:11
  10. echo $first->tzName;                               // Asia/Tokyo
  11. echo $second->toDateTimeString();                  // 2012-09-05 07:26:11
  12. echo $second->tzName;                              // America/Vancouver
  13.  
  14. var_dump($first->eq($second));                     // bool(true)
  15. var_dump($first->ne($second));                     // bool(false)
  16. var_dump($first->gt($second));                     // bool(false)
  17. var_dump($first->gte($second));                    // bool(true)
  18. var_dump($first->lt($second));                     // bool(false)
  19. var_dump($first->lte($second));                    // bool(true)
  20.  
  21. $first->setDateTime(2012, 1, 1, 0, 0, 0);
  22. $second->setDateTime(2012, 1, 1, 0, 0, 0);         // Remember tz is 'America/Vancouver'
  23.  
  24. var_dump($first->eq($second));                     // bool(false)
  25. var_dump($first->ne($second));                     // bool(true)
  26. var_dump($first->gt($second));                     // bool(false)
  27. var_dump($first->gte($second));                    // bool(false)
  28. var_dump($first->lt($second));                     // bool(true)
  29. var_dump($first->lte($second));                    // bool(true)

範囲比較


  1. <?php
  2. require 'vendor/autoload.php';
  3.  
  4. use Carbon\Carbon;
  5.  
  6. $first = Carbon::create(2012, 9, 5, 1);
  7. $second = Carbon::create(2012, 9, 5, 5);
  8. var_dump(Carbon::create(2012, 9, 5, 3)->between($first, $second));          // bool(true)
  9. var_dump(Carbon::create(2012, 9, 5, 5)->between($first, $second));          // bool(true)
  10. var_dump(Carbon::create(2012, 9, 5, 5)->between($first, $second, false));   // bool(false)

他には日時の差(diff)やis〜系メソッド(isBirthday()等の面白いメソッドもありました笑)で使えそうな機能もありますが、今回の記事ではこれくらいにしておきます。詳細は公式のドキュメント(http://carbon.nesbot.com/docs)をどうぞ。

・参考



- https://github.com/briannesbitt/Carbon/blob/master/src/Carbon/Carbon.php
- http://php.net/manual/ja/class.datetime.php
- http://php.net/manual/ja/function.date.php
- http://carbon.nesbot.com/docs