読者です 読者をやめる 読者になる 読者になる

letsspeak's diary

世界を大いに盛り上げるためのletsspeakの日記。

FuelPHPのserver_gmt_offsetの設定について調べてみた

前記事でstationwagonを動かすまでの手順をかいてみたところ、@kenji_sさんから、server_gmt_offsetは0のままでも良いのではという旨のご指摘を頂いたので、実際に自分でも調べてみました。

偉大なる参照記事様
ORMのObservers、タイムゾーン設定(Asia/Tokyo)で気をつける事

ほとんど上記記事の通りなのですが、設定が微妙に違ったので、少し解説が必要と感じたポイントなども加え流れをまとめておきます。

結論

サーバ側でロケール設定を日本時間に合わせてある場合、fuel/app/config.php のserver_gmt_offsetは初期値である 0 のまま変更しなくても良い。

下記のように date コマンドで現在時刻が表示される環境なら大丈夫かと思います。

$ date
2013年  3月  9日 土曜日 13:45:30 JST

(詳しく調べてはいませんが、最終的に php の strftime() 使用時の環境設定も関係しそうな気がします。)

調査前

まずは、fuel/app/config.php

 'server_gmt_offset'  => 3600 * 9,

の状態でstationwagonのarticleを作成してみました。

f:id:letsspeak:20130309145127p:plain

201303091304に投稿したunix time が 1362834283 となっています。
こちらの変換サイトで変換すると 2013/3/09 22:04:43 となっていますね。
まさにご指摘頂いた通り9時間分ずれてしまっていることがわかります。

created_at生成までの流れを追う

Model内でのarticle生成時の挙動を確認します。

fuel/app/classes/model/article.php
  protected static $_observers = array(
    'Orm\\Observer_CreatedAt' => array('before_insert'),
    'Orm\\Observer_UpdatedAt' => array('before_save'),
  );  

$_ovserversの設定により、Modelがデータベースに書き込まれる際の before_insert という信号で 'Orm\\Observer_CreatedAt の before_insert が呼ばれるようになっているようです。

中で何をしているか見てみます。

fuel/packages/orm/classes/observer/createdat.php
public function before_insert(Model $obj)
  {
    $obj->{$this->_property} = $this->_mysql_timestamp ? \Date::time()->format('mysql') : \Date::time()->get_timestamp();
  }

$objはModel自身、今回は fuel/app/classes/model/article.php の Model_Article を指すようになる仕組みで、$this->_property は何も指定が無ければ 'created_at' という値になるようです。

つまり、ここで自動的に Model_Articleの created_at に設定する日付を生成しているということです。

何も指定が無い場合は _mysql_timestamp が false になり、Date::time()->get_timestamp() が呼ばれます。
mysql_timestamp を true に設定していた場合は、 Date::time()->format('mysql') が呼ばれます。

Date::time()ってなに?

php初心者なので最初、Date::time() が何をやっているのか分からなかったのですが、

fuel/core/classes/date.php
   public static function forge($timestamp = null, $timezone = null)
   {
     return new static($timestamp, $timezone);
   }
 
   public static function time($timezone = null)
   {
     return static::forge(null, $timezone);
   }

Date自身がメモリ上に生成されなければ get_timestamp() や format('mysql') といったクラスメソッドを呼び出す事ができないので、このくだりでDate自身を生成しているようです。

Objective-Cで書くとこんな感じですね。

return [[Date time] format:@"mysql"];

あるいはもっとわかりやすく書くと

Date *date = [[[Date alloc] init] autorelease];
return [date Format:@"mysql"];

となるかと思います。

今回、time()の引数には何も指定されていないので、$timestamp = null, $timezone = null の状態で初期化されます。
初期化時にはルール上、下記の __construct() が呼ばれるようです。

fuel/core/classes/date.php
  public function __construct($timestamp = null, $timezone = null)
   {
     ! $timestamp and $timestamp = time() + static::$server_gmt_offset;
     ! $timezone and $timezone   = \Fuel::$timezone;
 
     $this->timestamp = $timestamp;
     $this->set_timezone($timezone);
   }

はい、やっと出ました static::$server_gmt_offset;
このタイミングで time() との加算値が $timestamp に格納されます。

Dateクラス内での日付生成挙動

それでは、format('mysql')時とget_timestamp()時の挙動を見てみます。
fuel/core/classes/date.php

  public function format($pattern_key = 'local', $timezone = null)
  {
    \Config::load('date', 'date');

    $pattern = \Config::get('date.patterns.'.$pattern_key, $pattern_key);

    // determine the timezone to switch to
    $timezone === true and $timezone = static::$display_timezone;
    is_string($timezone) or $timezone = $this->timezone;

    // Temporarily change timezone when different from default
    if (\Fuel::$timezone != $timezone)
    {   
      date_default_timezone_set($timezone);
    }   

    // Create output
    $output = strftime($pattern, $this->timestamp);

    // Change timezone back to default if changed previously
    if (\Fuel::$timezone != $timezone)
    {   
      date_default_timezone_set(\Fuel::$timezone);
    }   

    return $output;
  }

   public function get_timestamp()
  {
    return $this->timestamp;
  }

get_timestamp() 呼び出し時は $timestamp をそのまま返しています。
また、format() 呼び出し時は

    $output = strftime($pattern, $this->timestamp);

のくだりで$timestampを使用しています。
引数に 'format' 指定時の $pattern は下記にありました。

fuel/core/config/date.php
     'mysql'    => '%Y-%m-%d %H:%M:%S',

phpの対話シェルで確認

それぞれの挙動をphpの対話シェルで確認します。

time()
$ php -a
Interactive shell

php > echo time();
1362810492

unix time で帰ってきますので、先ほどの変換サイトで変換します。
サーバ側でロケール設定を日本時間に合わせてある場合、現在の日本時間が返ってくると思います。

strftime()
$ php -a
Interactive shell

php > echo strftime('%Y-%m-%d %H:%M:%S', time());
2013-03-09 13:40:52

下記警告が出ましたがとりあえず現在時刻が返ってくると思います。

PHP Warning:  strftime(): It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected 'Asia/Tokyo' for 'JST/9.0/no DST' instead in php shell code on line 1

server_gmt_offsetを修正

という訳でserver_gmt_offsetの値を 0 に修正して最終確認です。

fuel/app/config.php

 'server_gmt_offset'  => 0,

f:id:letsspeak:20130309154548p:plain
201303091424に投稿したunix time が 1362806678 となっています。
こちらの変換サイトで変換すると 2013/3/09 14:24:38 となっています。
ばっちりです!

参照記事との違いとデータベース構造

ORMのObservers、タイムゾーン設定(Asia/Tokyo)で気をつける事
上記参照記事の場合はModelsのObservers設定時に

protected static $_observers = array(
  'Orm\\Observer_CreatedAt' => array(
    'events' => array('before_insert'),
    'mysql_timestamp' => true, // datetime 型で挿入させる
    'property' => 'created', // テーブルカラム名を created に変更
  ),
);

というように mysql_timestamp, property などの引数が指定されていますが、stationwagonでは

fuel/app/classes/model/article.php
  protected static $_observers = array(
    'Orm\\Observer_CreatedAt' => array('before_insert'),
    'Orm\\Observer_UpdatedAt' => array('before_save'),
  );  

となっており、引数設定が行われていません。

先ほども書きましたが、

Orm\\Observer_CreatedAtの引数に何も指定が無い場合はDate::time()->get_timestamp() が呼ばれ、mysql_timestamp を true に設定していた場合は、 Date::time()->format('mysql') が呼ばれます。

この違いが発生している理由は、
前回記事に付け足したdatabase.mysqlにありました。

database.sql

CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
  `password` varchar(60) COLLATE utf8_unicode_ci DEFAULT NULL,
  `email` varchar(70) COLLATE utf8_unicode_ci DEFAULT NULL,
  `profile_fields` text COLLATE utf8_unicode_ci,
  `group` int(11) DEFAULT NULL,
  `last_login` bigint(20) DEFAULT NULL,
  `login_hash` tinytext COLLATE utf8_unicode_ci,
  PRIMARY KEY (`id`),
  UNIQUE KEY `username` (`username`),
  UNIQUE KEY `email` (`email`),
  `created_at` bigint(20) unsigned DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

他のテーブルからのコピペで created_at が bigint 型になっています。
$_observersの設定を顧みても、stationwagon としてはこのコピペは正解だったようです。

created_dtにdatatime型を使いたい場合は、
$_observersの設定で'mysql_timestamp' => true を指定する必要がありそうです。

気になった事

phpの対話シェルでstrftime()を実行した際に

PHP Warning: strftime(): It is not safe to rely on the system's timezone settings. You are *required* to use the date.timezone setting or the date_default_timezone_set() function. In case you used any of those methods and you are still getting this warning, you most likely misspelled the timezone identifier. We selected 'Asia/Tokyo' for 'JST/9.0/no DST' instead in php shell code on line 1

という警告が出てきました。
この調査は宿題にしますが、'mysql_timestamp' => true 時はstrftime() が使われるのでちょっと気になりますね。