FuelPHPでopauthを使って色んなログインに対応してみた
FuelPHPでopauthを使って色んなログインに対応してみました。
ログインのパターンは
1.通常のusernameとpasswordのログイン
2.Twitterのoauthログイン
3.Facebookのoauthログイン
です。
TwitterとFacebookのログインについてはfuel-opauthを使っていますが、こちらの参考ブログ記事にも記載されている通りnamespaceの設定に不具合があるためgithubでforkしたものをsubmodule化しています。
本家 https://github.com/andreoav/fuel-opauth本家
修正版 https://github.com/letsspeak/fuel-opauth
今回作成したり変更したファイルは一通りgistにもアップロードしています。
不具合はいつも通り触りながら修正していこうかと思っていますが、問題点などありましたらお気軽にご連絡ください。
gist
https://gist.github.com/letsspeak/5229245
fuel-opauthのsubmodule化
git submodule add https://github.com/letsspeak/fuel-opauth.git fuel/app/packages/opauth/
TIPS
githubからhttps://でsubmodule化した場合、内容の変更が発生したときのpush時に下記修正が必要なので注意。
fuel/packages/opauth/.git/config
- url = https://github.com/letsspeak/fuel-opauth + url = ssh://git@github.com/letsspeak/fuel-opauth.git
opauth.phpの設定
ソルトとTwitter/Facebookの開発者用のキーを登録する。
fuel/app/config/opauth.php
<?php namespace Opauth; return array( 'path' => '/auth/login/', 'callback_url' => '/auth/callback/', 'security_salt' => 'ランダムな文字列を生成して登録する', 'Strategy' => array( 'Facebook' => array( 'app_id' => 'Facebookで取得したアプリID / APIキー', 'app_secret' => 'Facebookで取得したアプリのシークレットキー' ), 'Twitter' => array( 'key' => 'Consumer key', 'secret' => 'Consumer secret' ), ), );
User/TwitterUser/FacebookUserを作成
oil g model user name:string password:string nickname:string email:string last_login:int oil g model twitteruser uid:string token:string secret:string user_id:int oil g model facebookuser uid:string token:string expires:int user_id:int
マイグレーションの変更点
ストレージエンジンは全てInnoDBに変更。
Userで一元管理することを想定してUser->last_login以外はすべて'null'=>trueに設定。
モデルクラス名をModel_TwitterUserに変更したいのでテーブル名をtwitter_usersに変更。
モデルクラス名をModel_FacebookUserに変更したいのでテーブル名をfacebook_usersに変更。
モデルの変更点
モデルクラス名をそれぞれModel_TwitterUser、Model_FacebookUserに変更。
Model_User、Model_TwitterUser、Model_FacebookUserのバリデーション追加。
authコントローラーを作成
通常ログイン時は引数無しで /auth/login/にPOSTする。
Twitterログイン時は/auth/login/twitter/を叩く。
Facebookログイン時は/auth/login/facebook/を叩く。
fuel/app/classes/controller/auth.php
<?php class Controller_Auth extends Controller { private $_config = null; private $_salt_length = null; private $_iteration_count = null; public function before() { if(!isset($this->_config)) { $this->_config = Config::load('opauth', 'opauth'); } $this->_salt_length = 32; $this->_iteration_count = 10; } // auth/login/* public function action_login($_provider = null, $method = null) { // 引数無し時は通常ログイン if (is_null($_provider)) return $this->normal_login(); // http://domainname/auth/login/twitter/oauth_callback?denied=signature if ($method === 'oauth_callback') { if (Input::get('denied')){ return $this->login_failed(); } } if(array_key_exists(Inflector::humanize($_provider), Arr::get($this->_config, 'Strategy'))) { $_oauth = new Opauth($this->_config, true); } else { return $this->login_failed(); } } // 通常ログイン public function normal_login() { $username = Input::post('username'); $password = Input::post('password'); // ユーザー名とパスワード空欄時はログインフォームを表示する if (is_null($username) and is_null($password)) { return Response::forge(View::forge('auth/form')); } // 認証 $query = Model_User::query()->where('name', $username); if ($query->count() === 0){ // 認証エラー $this->login_failed(); } // パスワードのハッシュ化 $user = $query->get_one(); $salt = substr($user->password, 0, $this->_salt_length); $enc_password = $salt.$password; for ($i = 0; $i < $this->_iteration_count; $i++) { $enc_password = sha1($enc_password); } if ($user->password === $salt.$enc_password){ // 認証成功 return $this->login_succeeded($user->id); }else{ // 認証エラー $this->login_failed(); } } public function action_signup() { $username = Input::post('username'); $password = Input::post('password'); // ユーザー名とパスワード空欄時はサインアップフォームを表示する if (is_null($username) and is_null($password)) { return Response::forge(View::forge('auth/signup')); } // サインアップ処理 // パスワードのハッシュ化 $salt = substr(md5(uniqid(rand(), true)), 0, $this->_salt_length); $enc_password = $salt.$password; for ($i = 0; $i < $this->_iteration_count; $i++) { $enc_password = sha1($enc_password); } // バリデーション $val = Model_User::validate('create'); $input = array( 'name' => $username, 'password' => $salt.$enc_password, 'last_login' => \Date::time()->get_timestamp(), ); if ($val->run($input)) { // バリデーション成功時 $user = Model_User::forge($input); if ($user and $user->save()) { // サインアップ成功時 return $this->login_succeeded($user->id); } else { // サインアップ失敗時 return $this->login_failed(); } } else { // バリデーション失敗時 $data['errors'] = $val->error(); return Response::forge(View::forge('auth/signup', $data)); } } public function action_test1() { return Response::forge(View::forge('auth/test')); } public function action_test2() { return Response::forge(View::forge('auth/test')); } public function action_test3() { return Response::forge(View::forge('auth/test')); } // Twitter / Facebook ログイン成功/失敗時に呼ばれる public function action_callback() { $_opauth = new Opauth($this->_config, false); switch($_opauth->env['callback_transport']) { case 'session': session_start(); $response = $_SESSION['opauth']; unset($_SESSION['opauth']); break; } if (array_key_exists('error', $response)) { return $this->login_failed(); // echo '<strong style="color: red;">Authentication error: </strong> Opauth returns error auth response.'."<br>\n"; } else { if (empty($response['auth']) || empty($response['timestamp']) || empty($response['signature']) || empty($response['auth']['provider']) || empty($response['auth']['uid'])) { return $this->login_failed(); // echo '<strong style="color: red;">Invalid auth response: </strong>Missing key auth response components.'."<br>\n"; } elseif (!$_opauth->validate(sha1(print_r($response['auth'], true)), $response['timestamp'], $response['signature'], $reason)) { return $this->login_failed(); // echo '<strong style="color: red;">Invalid auth response: </strong>'.$reason.".<br>\n"; } else { // Twitter / Facebook ログイン成功 return $this->opauth_login($response); } } } public function opauth_login($response = null) { $provider = $response['auth']['provider']; if ($provider === 'Twitter') return $this->twitter_login($response); if ($provider === 'Facebook') return $this->facebook_login($response); } public function twitter_login($response = null) { $uid = (string) $response['auth']['uid']; $query = Model_TwitterUser::query()->where('uid', $uid); if ($query->count() == 0) { // TwitterUser未登録の場合はサインアップ return $this->twitter_signup($response); } // TwitterUser登録済みの場合はログイン $twitter_user = $query->get_one(); return $this->login_succeeded($twitter_user->user_id); } public function facebook_login($response = null) { $uid = $response['auth']['uid']; $query = Model_FacebookUser::query()->where('uid', $uid); if ($query->count() == 0) { // FacebookUser未登録の場合はサインアップ return $this->facebook_signup($response); } // FacebookUser登録済みの場合はログイン $facebook_user = $query->get_one(); return $this->login_succeeded($facebook_user->user_id); } public function twitter_signup($response = null) { // バリデーション $val = Model_TwitterUser::validate('create'); $input = array( 'uid' => (string) $response['auth']['uid'], 'token' => $response['auth']['credentials']['token'], 'secret' => $response['auth']['credentials']['secret'], ); if ($val->run($input)) { // バリデーション成功時 $user = Model_User::forge(array( 'nickname' => $response['auth']['info']['nickname'], 'last_login' => \Date::time()->get_timestamp(), )); $twitter_user = Model_TwitterUser::forge($input); if ($user and $twitter_user) { // ユーザー生成成功 try { \DB::start_transaction(); if ($user->save() === false) { // User保存失敗 throw new \Exception('user save failed.'); } $twitter_user->user_id = $user->id; if ($twitter_user->save() === false) { // TwitterUser保存失敗 throw new \Exception('twitter_user save failed.'); } // UserとTwitterUserの保存成功 \DB::commit_transaction(); return $this->login_succeeded($user->id); } catch (\Exception $e) { \DB::rollback_transaction(); return $this->login_failed(); } } else { // ユーザー生成失敗 return $this->login_failed(); } } else { // バリデーション失敗時 return $this->login_failed(); } } public function facebook_signup($response = null) { // バリデーション $val = Model_FacebookUser::validate('create'); $expires = strtotime($response['auth']['credentials']['expires']); $input = array( 'uid' => (string) $response['auth']['uid'], 'token' => $response['auth']['credentials']['token'], 'expires' => $expires, ); if ($val->run($input)) { // バリデーション成功時 $user = Model_User::forge(array( 'nickname' => $response['auth']['info']['name'], 'last_login' => \Date::time()->get_timestamp(), )); $facebook_user = Model_FacebookUser::forge($input); if ($user and $facebook_user) { // ユーザー生成成功 try { \DB::start_transaction(); if ($user->save() === false) { // User保存失敗 throw new \Exception('user save failed.'); } $facebook_user->user_id = $user->id; if ($facebook_user->save() === false) { // FacebookUser保存失敗 throw new \Exception('facebook_user save failed.'); } // UserとFacebookUserの保存成功 \DB::commit_transaction(); return $this->login_succeeded($user->id); } catch (\Exception $e) { \DB::rollback_transaction(); return $this->login_failed(); } } else { // ユーザー生成失敗 return $this->login_failed(); } } else { // バリデーション失敗時 return $this->login_failed(); } } public function login_succeeded($user_id) { Session::set('user_id', $user_id); Response::redirect('auth/test1'); } public function login_failed() { return Response::redirect('auth/test1'); } public function action_logout() { Session::delete('user_id'); Response::redirect('auth/test1'); } }
懸念点など
・とりあえず作っただけなので全然脆弱な気がする。
・XSRF対策をしていない。
・ぜんぶlogin_failed()に飛ばしているけどいいの?
・特定のページでログインを経由して特定のページに復帰するようなことは考慮されていない。
・パスワードはいちおうハッシュ化してみた。
・Usersでの一括管理に意味があるのか...
初心者がFuelPHPのトランザクションに失敗した件
追記
記事を書いた後、
1.DBのクエリビルダーで更新してもダメ。
2.mysqlを直接コマンドから叩いてロールバックしてもダメ。
だったのでデータベースエンジンを確認してみたところ
mysql> show table status; +--------------+--------+---------+------------+------+----------------+-------------+------------------+--------------+-----------+----------------+---------------------+---------------------+------------+-----------------+----------+----------------+---------+ | Name | Engine | Version | Row_format | Rows | Avg_row_length | Data_length | Max_data_length | Index_length | Data_free | Auto_increment | Create_time | Update_time | Check_time | Collation | Checksum | Create_options | Comment | +--------------+--------+---------+------------+------+----------------+-------------+------------------+--------------+-----------+----------------+---------------------+---------------------+------------+-----------------+----------+----------------+---------+ | click_queues | MyISAM | 10 | Fixed | 14 | 21 | 294 | 5910974510923775 | 2048 | 0 | 15 | 2013-03-17 15:43:13 | 2013-03-18 00:47:41 | NULL | utf8_general_ci | NULL | | | | items | MyISAM | 10 | Dynamic | 1099 | 2440 | 2682076 | 281474976710655 | 13312 | 0 | 1100 | 2013-03-17 15:43:13 | 2013-03-18 00:45:08 | NULL | utf8_general_ci | NULL | | | | migration | MyISAM | 10 | Dynamic | 4 | 40 | 160 | 281474976710655 | 1024 | 0 | NULL | 2013-03-17 00:28:33 | 2013-03-17 14:27:45 | NULL | utf8_general_ci | NULL | | | | pick_counts | MyISAM | 10 | Fixed | 1099 | 29 | 31871 | 8162774324609023 | 13312 | 0 | 1100 | 2013-03-17 15:43:13 | 2013-03-18 00:54:38 | NULL | utf8_general_ci | NULL | | | | sources | MyISAM | 10 | Dynamic | 39 | 107 | 4184 | 281474976710655 | 2048 | 0 | 47 | 2013-03-17 00:28:33 | 2013-03-17 00:28:33 | NULL | utf8_general_ci | NULL | | | +--------------+--------+---------+------------+------+----------------+-------------+------------------+--------------+-----------+----------------+---------------------+---------------------+------------+-----------------+----------+----------------+---------+ 5 rows in set (0.00 sec)
追記2
下記のようにmigrationのすべてのcreate_table時に'InnoDB'を指定することで、下記のスクリプトのままトランザクションを実現することができました!
diff --git a/fuel/app/migrations/004_create_clickqueues.php b/fuel/app/migrations/004_create_clickqueues.php index 20f026b..dff08e5 100644 --- a/fuel/app/migrations/004_create_clickqueues.php +++ b/fuel/app/migrations/004_create_clickqueues.php @@ -13,7 +13,9 @@ class Create_clickqueues 'created_at' => array('constraint' => 11, 'type' => 'int', 'null' => true), 'updated_at' => array('constraint' => 11, 'type' => 'int', 'null' => true), - ), array('id')); + ), array('id'), + false, 'InnoDB', 'utf8_unicode_ci'); + } public function down()
はじめに
下記のロジックでクリック数の集計システムを作ってみました。
1.itemの一覧があって各itemのクリック時にidつきでAPIを叩く。
2.APIが叩かれたらClickQueueにitem_id, state=0を詰む。
3.一定周期でタスクclickpicker.phpを動かして集計する。
集計タスクは下記の通りです。
トランザクションを使ってClickQueueとItem(PickCount)の同期が確実の行われるように設定したつもりでした。
fuel/app/tasks/clickpicker.php
public function run($args = NULL) { $query = \Model_ClickQueue::query()->where('state', 0); while($query->count() > 0){ $click_queue = $query->get_one(); $click_queues = $query->where('item_id', $click_queue->item_id)->get(); $click_count = count($click_queues); $item = \Model_Item::find($click_queue->item_id); $pick_count = \Model_PickCount::find($item->pick_count_id); $pick_count->click = $pick_count->click + $click_count; foreach ($click_queues as $done_click_queue) { $done_click_queue->state = 1; } try { \DB::start_transaction(); $pick_count->save(); // throw new \Exception('hogehoge'); foreach ($click_queues as $done_click_queue) { $done_click_queue->save(); } \DB::commit_transaction(); } catch (\Exception $e) { \DB::rollback_transaction(); echo $e->getMessage().PHP_EOL; return; } echo 'item_id = '.$item->id.' +'.$click_count.' clicks ( total '.$pick_count->click.' clicks )'.PHP_EOL; $query = \Model_ClickQueue::query()->where('state', 0); } echo 'ClickPicker task successfully completed.'.PHP_EOL; } }
実装した結果
実際に動かしてみた結果は下記の通り。
$ oil r clickpicker item_id = 1 +1 clicks ( total 17 clicks ) ClickPicker task successfully completed.
とてもよく動いているように見えます。
が、コメントアウトしているデバッグ用の例外投げ処理を解除したうえで、item_id = 1067をクリックしてみたところ...
=> ClickQueueは未集計の状態
=> PickCountは集計された状態
となり、想定していたロールバックは正しく行われませんでした。
調査
下記内容はまだ調べている途中なので間違いがあるかもしれません!
半日かけてよくよく調べてみると、
DB::start_transaction();
DB::end_transaction();
DB::rollback_transaction();
は上記のような使い方はできないみたいです。
というのもfuel/ormは内部でPDOのインスタンスを生成しているっぽいのですが、他のorm modelとの共有機能はないみたいです。
ただfuel/orm model の機能として、
/** * Save the object and it's relations, create when necessary * * @param mixed $cascade * null = use default config, * bool = force/prevent cascade, * array cascades only the relations that are in the array */ public function save($cascade = null, $use_transaction = false)
というsaveのオプションがあるようなのでcascadeを正しく設定していればオプション付きの呼び出しで自動的にtransactionが行われるように思います。
おわりに
しかし冷静に考えてみると今回のようなキュー方式の場合、ItemとClickQueueはリレーショナルな関係にはならないのでsaveのオプションを使ってトランザクションを実現するのは難しそうです。
itemの更新があまり行われない状況ならキュー方式をやめてクリックごとにPickCountを+1ずつ加算してしまうか、そのままキューを使うのであれば、素直にDB::start_transaction()と一緒にそちらのクエリビルダを利用した方が良いのかも知れません。
初心者がFuelPHPのMySQLバックアップタスクを書いてみた
oil さんでマイグレーション時にデータベースのオプションが足りなくて書き直したりしていたとき、
oil r migrate:down oil r migrate:down
と戻しすぎてテーブルが吹っ飛んだのでバックアップタスクを書いてみました。
間違いなどありましたら是非是非ご指摘をお願いいたします。
gist letsspeak / mysqlbackup.php
fuel/app/tasks/mysqlbackup.php
<?php namespace Fuel\Tasks; class MySQLBackup { public static function system_ex($cmd, $stdin = "") { $descriptorspec = array( 0 => array("pipe", "r"), 1 => array("pipe", "w"), 2 => array("pipe", "w") ); $process = proc_open($cmd, $descriptorspec, $pipes); $result_message = ""; $error_message = ""; $return = null; if (is_resource($process)) { fputs($pipes[0], $stdin); fclose($pipes[0]); while ($error = fgets($pipes[2])){ $error_message .= $error; } while ($result = fgets($pipes[1])){ $result_message .= $result; } foreach ($pipes as $k=>$_rs){ if (is_resource($_rs)){ fclose($_rs); } } $return = proc_close($process); } return array( 'return' => $return, 'stdout' => $result_message, 'stderr' => $error_message, ); } public static function run($args = NULL) { \Config::load('db', true); $name = \Config::get('db.active'); $dsn = \Config::get('db.'.$name.'.connection.dsn'); $username = \Config::get('db.'.$name.'.connection.username'); $password = \Config::get('db.'.$name.'.connection.password'); @preg_match('/^(.+):host=(.+);dbname=(.+)$/i', $dsn, $matches); if (count($matches) !== 4){ $message = "Config dsn doesn't match.".PHP_EOL; $message .= 'check fuel/app/config/development/db.php'.PHP_EOL; echo $message; \Model_AlertMail::send_error('Fuel MySQLBackup Error', $message); return; } $dbtype = $matches[1]; $dbhost = $matches[2]; $dbname = $matches[3]; if (strtolower($dbtype) !== 'mysql') { $message = 'Config database type is not MySQL.'.PHP_EOL; $message .= 'check fuel/app/config/development/db.php'.PHP_EOL; echo $message; // \Model_AlertMail::send_error('Fuel MySQLBackup Error', $message); return; } $fileDir = '/var/log/fuel/'; $fileName = date('ymd').'_'.date('His').'.sql'; if ( ! file_exists($fileDir) ) { try { if ( ! @mkdir($fileDir) ){ $message = 'Cannnot create directory '.$fileDir.PHP_EOL; echo $message; \Model_AlertMail::send_error('Fuel MySQLBackup Error', $message); return; } } catch (\Exception $e) { echo "Cannnot create directory ",$fileDir.PHP_EOL; \Model_AlertMail::send_error('Fuel MySQLBackup Error', $message); return; } } $command = "mysqldump --default-character-set=binary ".$dbname." --host=".$dbhost." --user=".$username." --password=".$password." > ".$fileDir.$fileName; $return = MySQLBackup::system_ex($command); if ($return["stderr"] !== "") { $message = 'mysqldump failed for reasion:'.PHP_EOL.$return["stderr"]; \Model_AlertMail::send_error('Fuel MySQLBackup Error', $message); return; } echo 'MySQLBackup successfully completed.'.PHP_EOL; } } /* End of file tasks/MySQLBackup.php */
phpのstrftime()で警告が出る件とoil create fuelphpの失敗
前回の記事で
$ 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
というエラーが出る件で「宿題(キリッ」なんて言ってましたが、/etc/php.ini の date.timezone が指定されていない事が原因でした。
/etc/php.ini
date.timezone = "Asia/Tokyo"
日本標準時なら上記設定でOK。
ちなみに date.timezone が正しく設定されていない場合、oil create fuelphp も下記エラーで失敗します。
Submodule path 'fuel/packages/parser': checked out '593d93d0e4baf551ea14f59d8ecbba4ba6f28dea' Entering 'docs' Entering 'fuel/core' Entering 'fuel/packages/auth' Entering 'fuel/packages/email' Entering 'fuel/packages/log' Entering 'fuel/packages/oil' Entering 'fuel/packages/orm' Entering 'fuel/packages/parser' PHP Fatal error: Exception thrown without a stack frame in Unknown on line 0
ググってみたところ、こちらの素敵なブログに事例が書いてあって解決しました。
この場を借りて御礼申し上げます!
いやーとてつもなくダサいッ!
FuelPHPのserver_gmt_offsetの設定について調べてみた
前記事でstationwagonを動かすまでの手順をかいてみたところ、@kenji_sさんから、server_gmt_offsetは0のままでも良いのではという旨のご指摘を頂いたので、実際に自分でも調べてみました。
偉大なる参照記事様
ORMのObservers、タイムゾーン設定(Asia/Tokyo)で気をつける事
ほとんど上記記事の通りなのですが、設定が微妙に違ったので、少し解説が必要と感じたポイントなども加え流れをまとめておきます。
結論
調査前
まずは、fuel/app/config.php
'server_gmt_offset' => 3600 * 9,
の状態でstationwagonのarticleを作成してみました。
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,
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() が使われるのでちょっと気になりますね。
初心者がCentOSでFuelPHPのstationwagonを動かしてみた
Webアプリケーションを作ろうと、さくらのVPS (CentOS 6.3) で Ruby on Rails を弄ったりしていたのですが、Railsはどうも記述されていないルールが掴めないので、もう一度FuelPHPをはじめなおしてみました。
今回も意外と詰んだりしたので手順を残しておきます。
初心者なので間違っている設定などあるかもしれません。ご注意ください。
偉大なる先人様の参照記事
FuelPHP のサンプルアプリ「Stationwagon」をみてみる - A Day in Serenity @ kenjis
1.nginx と apache の連携
今回は既に複数のunicorn_railsと連携中のnginxさんにapacheの設定を追加しました。
apacheさんも既にhogeに割り当てられていたためバーチャルホストで対応します。
/etc/nginx/nginx.conf
upstream fuelhoge { server 127.0.0.1:8000; } upstream hoge { server 127.0.0.1:8001; } server { listen 80; server_name fuelhoge.jp.; access_log /var/log/nginx_fuelhoge_access.log; error_log /var/log/nginx_fuelhoge_error.log; proxy_connect_timeout 60; proxy_read_timeout 60; proxy_send_timeout 60; location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_pass http://fuelhoge; proxy_redirect off; } } server { listen 80; server_name hoge.jp; proxy_connect_timeout 60; proxy_read_timeout 60; proxy_send_timeout 60; location / { proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header Host $http_host; proxy_pass http://hoge; proxy_redirect off; } }
/etc/httpd/conf/httpd.conf
Listen 8000
Listen 8001
<VirtualHost *:8000>
ServerName fuelhoge.jp
DocumentRoot /var/www/fuelhoge
</VirtualHost>
<VirtualHost *:8001>
ServerName hoge.jp
DocumentRoot /var/www/hoge
</VirtualHost>
<Directory "/var/www/fuelhoge/stationwagon/public">
AllowOverride All
</Directory>
NameVirtualHost *:8001
(前回のmacでの設定時同様、AllowOverride All の設定を忘れていて少し詰みました。)
2.stationwagonのインストールとパーミッション設定
$ git clone git://github.com/abdelm/stationwagon.git $ cd stationwagon $ oil r install
ここでは操作ユーザーの書き込み権限が無くてgit cloneで詰みました。
作業ユーザーは既にapacheグループに追加済みでしたので、/var/www/fuelhoge/ のapacheグループの書き込み権限を有効にしました。
$ chmod 775 /var/www/fuelhoge
3.stationwagonのconfig.phpを設定
設定は参照元の記事の通りです。
--- a/fuel/app/config/config.php +++ b/fuel/app/config/config.php @@ -41,7 +41,7 @@ return array( * * Set this to false or remove if you using mod_rewrite. */ - 'index_file' => 'index.php', + 'index_file' => '', 'profiling' => false, @@ -71,7 +71,7 @@ return array( */ 'language' => 'en', // Default language 'language_fallback' => 'en', // Fallback language when file isn't available for default language - 'locale' => 'en_US', // PHP set_locale() setting, null to not set + 'locale' => 'ja_JP.utf8', // PHP set_locale() setting, null to not set 'encoding' => 'UTF-8', @@ -81,8 +81,8 @@ return array( * server_gmt_offset in seconds the server offset from gmt timestamp when time() is used * default_timezone optional, if you want to change the server's default timezone */ - 'server_gmt_offset' => 0, - 'default_timezone' => 'UTC', + 'server_gmt_offset' => 3600 * 9, + 'default_timezone' => 'Asia/Tokyo', /** * Logging Threshold. Can be set to any of the following: @@ -94,7 +94,7 @@ return array( * Fuel::L_INFO * Fuel::L_ALL */ - 'log_threshold' => Fuel::L_WARNING, + 'log_threshold' => Fuel::L_ALL, 'log_path' => APPPATH.'logs/', 'log_date_format' => 'Y-m-d H:i:s',
【注意】上記では server_gmt_offset を 3600*9 秒 = 9時間 に変更していますが、サーバ側でロケール設定を日本時間に合わせてある場合、変更せずに 0 のままで問題ないようです!
4.データベースの作成
データベースの作成も参照元記事と同じです。
MySQLにログインして下記コマンドを打ち込みます。
> CREATE USER 'stationwagon'@'localhost' IDENTIFIED BY '***'; > GRANT USAGE ON * . * TO 'stationwagon'@'localhost' IDENTIFIED BY '***' WITH MAX_QUERIES_PER_HOUR 0 MAX_CONNECTIONS_PER_HOUR 0 MAX_UPDATES_PER_HOUR 0 MAX_USER_CONNECTIONS 0 ; > CREATE DATABASE IF NOT EXISTS `stationwagon` ; > GRANT ALL PRIVILEGES ON `stationwagon` . * TO 'stationwagon'@'localhost';
テーブルの作成は面倒なので stationwagon/database.sql を流し込んだのですが、実際に動かして Sign Up を実行したときにusers テーブルに crated_at が無いというエラーが出たため、下記のように変更しました。
diff --git a/database.sql b/database.sql index a87b3aa..88da9b8 100644 --- a/database.sql +++ b/database.sql @@ -11,7 +11,8 @@ CREATE TABLE `users` ( `login_hash` tinytext COLLATE utf8_unicode_ci, PRIMARY KEY (`id`), UNIQUE KEY `username` (`username`), - UNIQUE KEY `email` (`email`) + UNIQUE KEY `email` (`email`), + `created_at` bigint(20) unsigned DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;
database.sqlをデータベース stationwagon へ流し込みます。
mysql -u stationwagon -p -h localhost stationwagon < database.sql
5.データベース接続設定
これも参照先の記事とまったく同じで大丈夫でした。
--- a/fuel/app/config/development/db.php +++ b/fuel/app/config/development/db.php @@ -7,8 +7,9 @@ return array( 'default' => array( 'connection' => array( 'dsn' => 'mysql:host=localhost;dbname=stationwagon', - 'username' => 'root', - 'password' => '', + 'username' => 'stationwagon', + 'password' => '***', ), + 'table_prefix' => 'sw_', ), );
6.mod_rewriteの設定
1に書いた通りAllowOverride Allが問題なければ大丈夫でした。
以上です。
想像以上に詰まりましたが、参照元の記事に加えて、まさかの自分の前回の記事がとても頼りになりました。
もし間違いなどがありましたらご指摘いただけると嬉しいです!
シンタックスハイライトをXcodeっぽくしてみた
カスタムCSSデザイン設定
.synComment { color:#3e9910 } .synConstant { color:#cc0000 } .synIdentifier { color:#454545 } .synPreProc { color:#7f3f11 } .synSpecial { color:#c000c0 } .synStatement { color:#cc16ad } .synType { color:#cc16ad }
結果
#import "CCLayer.h" #import "SRPG.h" #import "SRArea.h" @interface SRAreaLayer : CCLayer - (void)removeAllAreaSprite; - (void)drawArea:(SRArea*)area; @end // on "init" you need to initialize your instance -(id) init { // always call "super" init // Apple recommends to re-assign "self" with the "super" return value if( (self=[super init])) { // create map layer _mapLayer = [SRMapLayer node]; [self addChild:_mapLayer]; // create area layer _areaLayer = [SRAreaLayer node]; [self addChild:_areaLayer]; // create units self.unitArray = [CCArray array]; SRUnit *unit1 = [[SRUnit alloc] init]; unit1.spriteFilename = @"pc01_00.png"; unit1.fieldPoint = SRFieldPointMake(5, 5); unit1.moving = 3; [self.unitArray addObject:unit1]; SRUnit *unit2 = [[SRUnit alloc] init]; unit2.spriteFilename = @"pc01_00.png"; unit2.fieldPoint = SRFieldPointMake(2, 10); unit2.moving = 5; [self.unitArray addObject:unit2]; // create unit layer and set units _unitLayer = [SRUnitLayer node]; [_unitLayer setUnits:_unitArray]; [self addChild:_unitLayer]; self.selectedUnit = nil; self.phase = SRFieldScenePhaseNoAction; } return self; }