概要
PHP8.1 未満の環境における CakePHP の FrozenDate は、 UTC でない Timezone を使った場合に、日付差分が正しく取れないケースがある。
FrozenTime を使うようにするか、 PHP8.1 以降にバージョンを上げると解決する。
※タイトルでは PHP7 としているけど、 8.0 でもだめなはず。
PHP8.1未満で比較する場合
環境は PHP7.4 , CakePHP は 4.2.8 で試す。
たとえば 2021年1月1日から3月1日までの差分を求めるとする。
期待値は当然 2 だ。
$ php -v PHP 7.4.24 (cli) (built: Sep 23 2021 22:49:50) ( NTS ) Copyright (c) The PHP Group Zend Engine v3.4.0, Copyright (c) Zend Technologies with Zend OPcache v7.4.24, Copyright (c), by Zend Technologies with Xdebug v2.9.8, Copyright (c) 2002-2020, by Derick Rethans $ bin/cake version 4.2.8
FrozenTime を使った場合
以前からあった diffInMonths は TimeZone の問題を抱えていたが、diffInMonthsIgnoreTimezone が提供されるようになったことで、期待通りの結果が得られるようになった。
<?php use Cake\I18n\FrozenTime; // UTC 以外にする date_default_timezone_set('Asia/Tokyo'); $from = FrozenTime::create(2021, 1, 1); $to = FrozenTime::create(2021, 3, 1); // UTC 以外だと 1 が返ってきてしまうのでだめ $result1 = $from->diffInMonths($to); // 期待通り! $result2 = $from->diffInMonthsIgnoreTimezone($to);
FrozenDate を使った場合
ところが FrozenDate を使うと、どうにも 1 が返ってきてしまう。
<?php use Cake\I18n\FrozenDate; // UTC 以外にする date_default_timezone_set('Asia/Tokyo'); $from = FrozenDate::create(2021, 1, 1); $to = FrozenDate::create(2021, 3, 1); // IgnoreTimezone しても 1 が返ってきてしまう! $result = $from->diffInMonthsIgnoreTimezone($to);
原因
前述の Issue にコメントされているのだが、どうやらそもそも DateTime::diff に問題があったようで、バグ報告されている。
このバグは既に対応済で、 PHP8.1 のリリースに含まれている。
- PHP :: Bug #79452 :: DateTime::diff() generates months differently between time zones
- PHP: PHP 8 ChangeLog
PHP8.1で比較する場合
環境は PHP8.1 , CakePHP は 4.3.2 で試す。
$ php -v PHP 8.1.0 (cli) (built: Nov 30 2021 06:10:58) (NTS) Copyright (c) The PHP Group Zend Engine v4.1.0, Copyright (c) Zend Technologies $ bin/cake version 4.3.2
この環境で試すと、期待通りの結果が得られる。
(コードは同じもの)
<?php use Cake\I18n\FrozenDate; // UTC 以外にする date_default_timezone_set('Asia/Tokyo'); $from = FrozenDate::create(2021, 1, 1); $to = FrozenDate::create(2021, 3, 1); // 無事 2 が返ってくるようになった $result = $from->diffInMonthsIgnoreTimezone($to);
宣伝
今回の検証にあたって、以前作った Cake 用の Docker Image に PHP8.1 版を追加しておいたので、よかったら使ってみてください。
参考
dateInMonths の罠については↓らへんの記事が参考になった。