PHPでdate()関数を素朴に使うと日付またぎや秒単位のずれに悩まされることがあります。バッチ処理でデータベースのupdate_atがずれたり、たまたま23時59分58秒に実行開始して0時0分2秒で終わる、などの時です。
date()はデフォルトでその実行タイミングでの時刻を使用するので、2回以上実行するとき、その値が同じとは限らないのです。
その克服方法としてはextensionを書いてしまうというのが根本解決ですが、いかんせん導入のハードルが高すぎます。ここはライトな方法としてClockWrapperを使う方法を考えてみます。
ClockWrapperとは
ClockWrapperはマーチン・ファウラーのサイトにちょろっとだけ言及されているもので、xUnitテストを意識して「システム時計を直接見るなよ」と書いているだけのものです。
PHPではdate()関数の第2引数など、timestampを渡さなければデフォルトでシステム時計を見るようになっています。このデフォルトが「現在の時刻」ではなく例えば「処理開始時刻」であれば日付またぎ問題は解決し、「任意の設定時刻」であればxUnitで特定の時刻でのテストが可能になります。
PHP5.1以上では $_SERVER['REQUEST_TIME']
で処理開始時刻のタイムスタンプを取得できますので、date()を呼び出すたびに第2引数にこれを渡してやれば一応の解決は見ます。
$datetime = date('Y-m-d H:i:s', $_SERVER['REQUEST_TIME']);
呼び出すたびに引数を書くのは間違いの元なので、クラスを作りましょうか。
class ClockWrapper {
function __construct($timestamp = null)
{
$this->timestamp = $timestamp ?? $_SERVER['REQUEST_TIME'];
}
function date($format)
{
return date($format, $this->timestamp);
}
}
一回作ってしまえば使いまわせるとはいえ、DateTimeクラスの存在を知っていると車輪の再発明感がすごいですね。DateTimeクラスがシングルトンで使えれば解決です。
class ClockWrapper {
static function get_instance()
{
static $datetime = null;
if (!$datetime) {
$datetime = new DateTime();
}
return $datetime;
}
}
で、これがCodeIgniterだともっと簡単にできます。
class MY_Controller extends CI_Controller
{
function __construct()
{
parent::__construct();
$this->load->library('DateTime', null, 'clock');
}
}
こうしておくと、時計を使いたい各所で $this->clock->format('Y-m-d H:i:s');
とすることができ、xUnitの導入にも耐えやすいです。
これは $this->load->library('DateTime', null, 'clock');
によって作られたインスタンスが $this->clock
にアサインされ、それがアプリケーションが生きている間は永続的に1つだけが存在することによって実現しています。つまりこれはサービスロケータ……というよりレジストリパターンです。
xUnitで特定の日付を指定したい場合は get_instance()->clock->modify()
で変更可能です。
これはこれでOK
当初の課題であった「日付またぎ」「秒ずれ」「任意の日時指定」が解決していますので、CodeIgniter3ではこれが一番簡便かつ分かりやすい方法と思います。
「もっと汎用的に」とか「$_SERVER['REQUEST_TIME']
じゃない」とか考えるといろいろ突っ込みどころはあるのですが、過度の抽象化は可読性を下げ、かえってメンテナンス工数を取ります。これはこれで目的は達成されてますしひどいデメリットもないので、抽象化はほどほどにしましょう。
次回はこれをもとに CI_Loader はどこまで使い倒せるかについて考えてみたいと思います。
最近のコメント