yiiでJSONを返すAPI的なものを作る

いいいーーー。


はい。yiiってのがあるらしいです。php のframeworkです。


Yii Framework: Best for Web 2.0 Development
初めに: Yii とは何か | The Definitive Guide to Yii | Yii Framework


軽量で使いやすそうで、MVCらしいので、とりあえず使ってみる事に。
やりたい事は、param受け取ったら、処理して、結果をJSONで返すAPI的なもの。

install

こっからダウンロード
Download Yii Framework | Yii Framework
そして、解凍すれば準備は完了。


コマンドで雛形を呼び出してみます

% cd WebRoot
% php YiiRoot/framework/yiic.php webapp testdrive

apacheでも何でも良いですが、Webから見れる箇所をWebRootと便宜上記述してます

http://hostname/testdrive/index.php

これで見れるはず。
どうですかね?

ちなみに自分の場合は、php errorが出ました。

date(): 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

これは、単純に、date関数使うなら、timezone指定せー、と言っているだけなので、指定すると解決する。
index.php

date_default_timezone_set('Asia/Tokyo');

で、直ります。


この状態で、問い合わせメールも飛ぶし、ログイン機能も使えます。便利ね。

JSONを返す

基本的に全部render()的な関数が自動で呼ばれてしまうので、json返そうとすると、ちょいっといじらないといけません。


controllerの基本的な使い方を見ると、メソッドの最後にrender関数を呼んでます。

 28     public function actionIndex()
 29     {
 30         // renders the view file 'protected/views/site/index.php'
 31         // using the default layout 'protected/views/layouts/main.php'
 32         $this->render('index');
 33     }

今回の修正には関係ないですが、処理フローを理解するためも、render関数を探してみましょう。


APIの記述を、

testdrive/protected/controllers/ApiController.php

に書く事にします。
controllerの継承は

yii/framework/web/CBaseController.php

yii/framework/web/CController.php

testdrive/protected/components/Controller.php

testdrive/protected/controllers/ApiController.php

というような感じになっていて、render関数は、CController.phpにあります。

grep 'function render' framework/web/CController.php | grep -v '\*'
        public function render($view,$data=null,$return=false)
        public function renderText($text,$return=false)
        public function renderPartial($view,$data=null,$return=false,$processOutput=false)
        public function renderClip($name,$params=array(),$return=false)
        public function renderDynamic($callback)
        public function renderDynamicInternal($callback,$params)

frameworkの中身を変えたくはないので、components/Controller.php 内で render関数を overrideするか、renderJSON関数を作ってあげましょう。


今回は後者の対応にしました。

renderJSON関数

    /** 
     * json用render
     *
     **/
    public function renderJSON($data)
    {   
        $this->layout=false;
        header('Content-type: application/json');
        echo CJavaScript::jsonEncode($data);
        Yii::app()->end(); 
    }

まずはlayoutをfalseに設定しつつ、json用のheaderを吐きます。


jsonへのencodeは、標準関数で

json_encode($arr);

としても良いですが、frameworkがCJavaScriptというクラスを用意してくれているので、その関数を使ってみました。


最後に、endで、よけいな処理が続かないようにします。

INSERT 〜〜 ON DUPLICATE 時における、LAST_INSERT_ID()の挙動

INSERT 〜〜 ON DUPLICATE も LAST_INSERT_ID() も便利な関数なので、よく使わせてもらっています。
しかしこれらの関数を同時に利用した場合に、MySQL 5.1.12 より前のバージョンの場合に少し困る事があります。

autoincrementを利用していた場合に、LAST_INESRT_ID()の返す値が意図した値ではない場合があります。

検証

例えば下記のようなテーブル構造の場合を考えてみましょう。

CREATE TABLE `insert_test` (
  `id` int(11) unsigned NOT NULL auto_increment,
  `test_id` int(11) NOT NULL,
  `test_name` varchar(10) NOT NULL default '',
  PRIMARY KEY  (`id`),
  UNIQUE KEY (`test_id`)
);


さて、データをinsertしてみます。

INSERT INTO insert_test (test_id,test_name) value(1,"hoge");
SELECT LAST_INSERT_ID();

この処理の結果は、下記の通り

mysql> INSERT INTO insert_test (test_id,test_name) value(1,"hoge");
Query OK, 1 row affected (0.37 sec)

mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                1 | 
+------------------+
1 row in set (0.00 sec)

これは意図した通りの動きですね。


次に、ON DUPLICATEを使ってみます。

INSERT INTO insert_test (test_id,test_name) value(2,"fuga") ON DUPLICATE KEY UPDATE test_name = 'foo';
SELECT LAST_INSERT_ID();
mysql> INSERT INTO insert_test (test_id,test_name) value(2,"fuga") ON DUPLICATE KEY UPDATE test_name = 'foo';
Query OK, 1 row affected (0.00 sec)

mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                2 | 
+------------------+
1 row in set (0.00 sec)

これも意図した通り、最後にinsertされた2というidが帰ってきました。
さて、問題は次のパターンです。

INSERT INTO insert_test (test_id,test_name) value(2,"foo") ON DUPLICATE KEY UPDATE test_name = 'foo';
SELECT LAST_INSERT_ID();

この場合、UNIQUE_KEYが設定されているtest_idの値(2)はすでに存在するため、insert処理ではなく、update処理がかかります。

mysql> INSERT INTO insert_test (test_id,test_name) value(2,"foo") ON DUPLICATE KEY UPDATE test_name = 'foo';
Query OK, 2 rows affected (0.00 sec)

mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                3 | 
+------------------+
1 row in set (0.00 sec)

お。update処理をしたはずなのに、LAST_INSERT_IDの値は、incrementされてますね。
ためしに全データを見てみても、

mysql> SELECT * FROM insert_test;
+----+---------+-----------+
| id | test_id | test_name |
+----+---------+-----------+
|  1 |       1 | hoge      | 
|  2 |       2 | foo       | 
+----+---------+-----------+
2 rows in set (0.00 sec)

2件しかありません。

解決方法

できればinsertした場合も、updateした場合も、最後に処理したidを返してもらいたいもの。。
これを実現するには、SQLを下記のように修正します。

INSERT INTO insert_test (test_id,test_name) value(2,"foo") ON DUPLICATE KEY UPDATE test_name = 'foo', id = LAST_INSERT_ID(id);
SELECT LAST_INSERT_ID();

試してみましょう。

mysql> INSERT INTO insert_test (test_id,test_name) value(2,"foo") ON DUPLICATE KEY UPDATE test_name = 'foo', id = LAST_INSERT_ID(id);
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT LAST_INSERT_ID();
+------------------+
| LAST_INSERT_ID() |
+------------------+
|                2 | 
+------------------+
1 row in set (0.00 sec)

意図した動きになりました。


ま、mysqlのversionをさっさとあげろって話かもしれませんが、仕事上そう簡単にversionあげれない方もいると思うので。
ここに書いた事は、MySQL :: MySQL 5.1 リファレンスマニュアル (オンラインヘルプ) :: 8.2.5.3 INSERT ... ON DUPLICATE KEY UPDATE 構文 こちらに詳しく書いてあります。

Gihyo.jpの記事を印刷モードにするGreaseMoneky

勉強用に印刷したかったけど、印刷モードなかったので、グリモンでぐりぐりしてみた。


かなり適当な作り。
印刷モードと言っても、適当にいらない項目消しただけですが。。

install

ここにある
Gihyo.jpを印刷モード user.js

使い方

ごめんなさい。
エレメント追加すんの面倒で、かなり手抜きました。。。。


サブタイトルをクリックすると印刷モードになります。。。


これでできる! クロスブラウザJavaScript入門|gihyo.jp … 技術評論社
このページだったら、【これでできる! クロスブラウザJavaScript入門】を押すと印刷モードです

Mac OS X Snow Leopard に titanium mobile SDK と iOS SDK と android SDK を setup するまでのメモ

基本ここみる
HTML+JavaScriptでiPhone/Androidアプリを作れるTitanium Mobileとは(1/3) - @IT
ここの言う通りにすれば良いのだけど、ちょこちょこバージョン違うので思い通り行かないところあったので、メモ。
スクリーンショットは面倒でとってないので、自分用メモかな。

titanium mobile SDK

まずはダウンロード。
Sign Up for an Appcelerator Developer Account
アカウント設定が必要だが、それはダウンロード後の初回起動時でよい。
installされたら、適当にアプリケーションにコピーして、Titanium Developerを起動。
アカウント設定をしたら、とりあえずおk。

iOS SDK

apple id でログイン後
iOS Dev Center - Apple Developer
Xcodeも必要なので、【Xcode 3.2.6 and iOS SDK 4.3】と書いてあるリンクからダウンロード
4Gぐらいあるので、DL中は他の事やったほうがよい。
ダウンロード後はinstall。

android SDK

Android SDK | Android Developers
ここからMac版を落とす。
download後は、解凍して、解凍したフォルダ毎、適当な場所に置く。

起動してみる

実際はまだ準備は整ってないけど、ここらでTitanium Developerを起動してみる
Titanium Developerの使い方は一番上に張った解説記事がやってくれるので良いとして、【New Project】を押してみる
【project type】に【mobile】を指定。
すると、iOS SDKandroid SDKを自動で探してくれる。
iOS SDK はinstallが完了していれば、そのままTitanium Developerが自動認識してくれる。
android SDK はそのままだとエラーになるので、PATHを指定してやる必要がある。


【Edit Profile】より、【android SDK】の部分を選択。
android SDKの解凍したフォルダを指定する。

エラー

さて、ここからエラーが出たので、解法を下記に。
とりあえず、こんなんでました

【Couldn't find adb or android in your SDK's "tools" directory. You may need to install a newer version of the SDK tools.】

これは、android SDKの構成がversionによって変わってしまったかららしい
Cannot get Android SDK to work on OSX » Community Questions & Answers » Appcelerator Developer Center
Titanium Mobileで Android SDK の環境設定につまずく | jmblog.jp

/Users/[user name]/dev/android-sdk/platform-tools/
に入っている “adb” を

/Users/[user name]/dev/android-sdk/tools/
にコピーすればOKとのこと。

らしいです。

あれ?platform-toolsが空だ

ところで、自分の環境では、platform-tools ディレクトリが空な事に気づく。
なんかまだ根本的に間違っている事がある見たいよ。。


というわけで、ぐぐって、下記サイト様に出会う

ダウンロードしたら解凍してできたフォルダを任意の場所に移す。今回はアプリケーションフォルダにした。コマンドラインは今回一切必要ない。
フォルダをアプリケーションフォルダに移動したら、SDK フォルダ/tools/android をダブルクリック。ターミナルが開き、Android SDK and AVD Manager が起動する。

あ。なんか、そもそもandroid SDKのダウンロードって完了してなかったっぽいね。
アホか俺は。

というわけで、こちらの記事通り、入れたいもの入れていく。
最新版の SDK Platform android 3.1, API 12 と Android SDK Platform-tools を入れてみる


と、platform-tools 以下に大量にファイルがでけとる。
よかったよかった。
これで、”adb”の場所を移動できる。


さて、”adb”を移動して、再び【New Project】を選択してみると、今度は別のエラー

【Couldn't find Android API v4 (or 1.6) in your "platforms" directory. Try running the android tool and installing API v4 and Google APIs v4】

なんか、API v4 が必要らしい。
というわけで、再び tools/android を起動して、 SDK Platform android 1.6, API 4 をinstall


完了。
エラーはでなくなりました。

さくらインターネットのレンタルサーバでcakephpを利用する場合のはまりどころ。。

もうはまるのは嫌です。。

  1. さくらインターネットのレンサバ
  2. cake 1.2.6以上を使っている
  3. 独自ドメイン

Internal Server Errorとなり表示できない

一番初めにはまるところ。
多くの先駆者たちもはまっているので、ネットに情報があふれている。
ただし、cakeのバージョンによって微妙に違う挙動を示す事もあるので、バージョンによっては死ねる。


Internal Server Errorがでていて、エラーログには

mod_rewrite: maximum number of internal redirects reached.
Assuming configuration error.
Use 'RewriteOptions MaxRedirects' to increase the limit if neccessary.

こんなものが。


これは、.htaccessmod_rewriteの記述方法の問題です。
rewrite_baseの記述を追加すると、直ります。

こちら
"さくらインターネットで404エラー" フォーラム - CakePHP Users in Japan
に書いてある通りに設定しましょう。

indexは見えるのに、actionは400 Bad Requestとなる

これは独自ドメインの設定方法の問題っぽいです。
さくらサーバ マルチドメイン CakePHP | ラスタッタCakePHP
こちらに記述してある通り、さくらでの独自ドメインの設定方法のパスの最後に、「/」を入れると、駄目なようです。


「/」はずすだけで解決しました。。

Macでchromeを利用するとき知ってるとちょっと便利なショートカット

個人的にちょっと便利だと思っている物を何個か紹介。

タブ間を移動(alt + command + 左右)

alt と command を押しながら左右を押す事で、タブ間を移動できます。

履歴一覧を表示(command + y)

新しいタブを開けば、最近閉じた履歴は数件出てきますが、「今日の午前中見たページをまた見たい」みたいな時は、便利かと

ブックマークマネージャ(alt + command + b)

そのまま

閉じたタブを開く(shift + command + t)

直前に閉じたタグを開いてくれます。
連続で押せば、10個前のタブまで復活させてくれます。
間違ってchrome全体を落としちゃった場合も、これによって閉じる前の状態のタブ(最大10個?)を復活できます

ブラウザバック(command + ←)

ブラウザバックはcommand + 左
command + 右で、先に進めます

ページのトップへ(command + ↑)

command + ↓でページの一番下へ

お決まりのショートカットたち

新しいタブを開く(command + t)
現在のタブを閉じる(command + w)
chrome全体を落とす(command + q)

simplexml_load_fileにtimeoutを利用したい場合の代替方法

simplexml_load_fileは便利なんですが、細かいオプションの設定などができなくて苦労する事があります。


今回はある程度レスポンスが遅い事が予測されるapiを利用していたのですが、たまに結果がそのままロストして帰ってこない事がありました。
しかしprocessはresponseを待ち続け、結局processがずっと走り続けちゃいました。

 69             $xml_data = simplexml_load_string($api_url);
 70             if ($xml_data && count($xml_data->xpath('fault')) == 0) {
 71                 //処理

curlとsimplexml_load_stringの併用

simplexml_load_fileの利用をあきらめ、curlとの併用を試みます。

 63             // timeoutを設定するために、curlを利用
 64             $ch = curl_init($api_url);
 65             curl_setopt($ch, CURLOPT_TIMEOUT, 300);
 66             curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
 67             curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); // 戻り値を文字列で
 68             $xml_raw = curl_exec($ch);
 69             $xml_data = simplexml_load_string($xml_raw);
 70             if ($xml_data && count($xml_data->xpath('fault')) == 0) {
 71                 // 処理

curlに限らず、HTTP/Requestなどでもそうですが、connection用のtimeoutと、read用のtimeoutがあるのでご注意を。
特に理由がなければ、両方設定しておけば良いかと。


CURLOPT_RETURNTRANSFER を設定しないと、curl_execの戻り値が true になってしまいます。
文字列を返してもらって、simplexml_load_stringでパースしましょう。

stream_set_timeout

stream_set_timeoutを利用する方法もあるようです。
Re: [PHP] simpleXML - simplexml_load_file() timeout? [Resolved]


stream_set_timeoutのほうは動作確認してませんが、きっと動くんじゃないかと、淡い期待だけ抱いてます。