2012年12月27日木曜日

[Fireworks]ツールパネルのサブメニューを表示するには

ツールパネルの黒三角がついているアイコンは、サブメニューが表示されるはずなんだけど、表示される時とされない時があって、なんでだろうと思っていたら、どうやら長押し(クリック)しないと表示されないみたいですね。自分はクリック一瞬なので表示されていなかったようです。すごく基本なのに検索しても出てこない。

2012年12月26日水曜日

[AWS]EC2 Command Line Toolsを導入した。

以下を参照してできました。(英語)
 
Setting Up the Amazon EC2 Command Line Tools

ただ、Windows XPでやっていますが、コマンドプロンプトでsetxが使えなかったので、
環境変数の設定はGUI、「システムのプロパティ」→「環境変数」で行いました。

また、環境変数のパスをクオテーションで囲む場合、シングルでやるとだめで
ダブルクオテーションだといけました。

2012年12月20日木曜日

[AJAX]ajaxで取得したレスポンス内のjavascriptはデバッグできないので

Ajaxで取得したHtml等の中に書かれているjavascriptはFirebugなどで
デバッグすることができない。

なので、なるべくソースはもともとあるjavascript(Ajaxではなく最初にそのページを
表示した際に読み込まれるjavascript)内に関数として記述しておき、Ajaxで取得した
javascriptからはその関数を呼び出すだけ、という形にしておけば、デバッグが可能だ。

[JavaScript]htmlのタグIDと同じ名前の変数を作ってはいけない

下記で説明されているとおりとなります。

IE8で「オブジェクトでサポートされていないプロパティまたはメソッドです」と表示される

変数宣言する際にvarをつければエラーは発生しないのだが、
タグIDの方が無効になってしまうので、やはり違う名前をつけるべき。

[JavaScript]普通のfor文とfor-in文の微妙な違い

JavaScriptで配列やオブジェクトを扱う際に、普通のfor文とfor-in文の微妙な違いについて
紹介する。

■一次元配列の場合。
・普通のfor文

定義有り→エラーが発生しない。
list=new Array();
for(i=0;i<list.length;i++){
    alert(list[i]);
}

定義なし→エラーが発生する。
エラー: 'list' は宣言されていません。
for(i=0;i<list.length;i++){
    alert(list[i]);
}

・for-in文

定義あり→エラーが発生しない。
list=new Array();
for(var item in list){
    alert(list[item]);
}

定義なし→エラーが発生する。
エラー: 'list' は宣言されていません。
for(var item in list){
    alert(list[item]);
} 

つまり、上記のように一次元配列においての挙動は同じである。

■多次元配列の場合
・普通のfor文

二次元目までの定義あり→エラーが発生しない。
list=new Array();
list["a"]=new Array();
for(i=0;i<list["a"].length;i++){
    alert(list["a"][i]);
} 

二次元目までの定義なし(一次元目のみ定義)→エラーが発生する。
エラー: 'list.a.length' は Null またはオブジェクトではありません。 
list=new Array();
for(i=0;i<list["a"].length;i++){
    alert(list["a"][i]);
} 

・for-in文

二次元目までの定義あり→エラーが発生しない。
list=new Array();
list["a"]=new Array();
for(var item in list["a"]){
    alert(list["a"][item]);
}

二次元目までの定義なし(一次元目のみ定義)→エラーが発生しない。
list=new Array();
for(var item in list["a"]){
    alert(list["a"][item]);
}

つまり、for-in文で多次元配列を扱う場合は、2次元目の配列を定義していなくても
なぜかエラーが発生しない。

ちなみ三次元にしたところ、

以下のように一次元のみ定義だとエラーが発生。
エラー: 'list.a.b' は Null またはオブジェクトではありません。
list=new Array();
for(var item in list["a"]["b"]){
    alert(list[item]["a"]["b"][item]);
} 

二次元まで定義したらエラーが発生しなくなる。
list=new Array();
list["a"]=new Array();
for(var item in list["a"]["b"]){
    alert(list[item]["a"]["b"]);
}

■結論
①普通のfor文でx次元配列を扱う場合、x次元まで定義する必要がある。
②for-in文でx次元配列を扱う場合は、x-1次元まで定義すればよい。

※①はlengthを参照するのでx次元目まで定義していないとエラーが発生するのはかるが、
  ②でx次元目まで定義していないのにエラーが発生しない理由がわからない。


2012年12月19日水曜日

[Windows]LAN上のサーバにつながらない時の注意点

BuffaloのNASになぜか接続できないなぁ、と3時間位試行錯誤
していて今日気づいたのですが、「 Microsoft ネットワーク用クライアント」が
無効になっていました。
有効にしたところ接続できるようにまりました。
たぶん先日この辺いじっちゃったときに無効にしたんだろうな。

以下で紹介されています。

 Microsoft ネットワークへの接続を無効にする

2012年12月12日水曜日

[PHP]演算子の優先順位

以下のサイトで説明していただいているように、演算子には優先順位があります。

演算子の優先順位

今日以下のようなコードを書いていたのですが、

$votable_flg = !empty($auth) and $check <1 and $theme['Theme']['kind']==1;


$votable_flgには"!empty($auth)"の値(真偽値)しか代入されていませんでした。
"or"や"and"より"="が優先されて処理されるためのようです。
なので、"="の右辺を以下のように丸括弧でくくると、期待通りの値が代入されました。

$votable_flg = (!empty($auth) and $check <1 and $theme['Theme']['kind']==1);

[Excel]複数系列の線グラフの作り方(表に不要な列がある場合)

例えば以下のような表がある時、日付で変動するA、B、Cさんの情報を全て
同じ折れ線グラフで見たい場合、複数系列のグラフを作成する。
その際、横軸には日付が来る想定だ。
ただ、「曜日」列と「合計」列は特にグラフに入れる必要がない、とする。



その場合はまず表を全て選択し、「グラフウィザード」ボタンを押す。


そのままだと以下のように、「合計」という線ができてしまい、また、横軸の項目に
曜日も入ってしまう。

 そこで、「系列」タブをクリックし、系列の欄から不要な「合計」を削除し、
「項目軸ラベルに使用」欄の横軸に使う項目を選びなおすため、右側のボタンをクリック。


 以下のように、横軸の項目に使用したいセル(A2~A8)を選択。
※見出しの「日付」(A1)は選択に含めない。



 完了ボタンを押すと、以下の用に「曜日」列と「合計」列のないグラフが作れる。



※なんか見た目のバランスの悪いグラフですね。適宜サイズの変更などはしたほうがいいですね。
  あと、Excel2000を使用しています。

※最初の表を選択する時にCtrlキーを押して日付の列とデータの列を分けて選択し、
 曜日と合計を選択しないようにすればできる的な記事をどこかでみたのだけど、
 できなかったので、この方法を採用しています。



2012年12月7日金曜日

[jQuery]$.ajaxのオプションcompleteとsucceedの違い

completeオプションを使用した場合は、jsonからjavascriptの配列に変換する
処理が以下の3行目のように必要だった。

$.ajax({
 complete:function (data, textStatus) {
  data = $.parseJSON(data.responseText);
  if (data.redirect) {
              window.location.href = data.redirect;
         } else {
   $.alerts.dialogClass = "custom1";
   jAlert(data.message,"確認して下さい",clearAltCls);
         }
 },
 data:postData,
 type:"post",
 url:"\/rakuzon\/themes\/itemAdd"
});
だが、以下のようにsuccessを使用した場合は、11行目で指定したデータタイプに
自動的に変換されている。便利。

$.ajax({
 success:function (data, textStatus) {
  if (data.redirect) {
              window.location.href = data.redirect;
         } else {
   $.alerts.dialogClass = "custom1";
   jAlert(data.message,"確認して下さい",clearAltCls);
         }
 },
 data:postData,
 dataType:"json",
 type:"post",
 url:"\/rakuzon\/themes\/itemAdd"
}); 

ちなみに、予断だが、ajaxの結果にレスポンス結果によってリダイレクトしたい
場合は、上記の4行目のようにすればよい。

2012年12月6日木曜日

[jQuery]「jQuery Alert Dialogs」での$(this)の参照が変わる?

検証はしていないので、原因が違ったらすみません。
以下の「jQuery Alert Dialogs」を使っています。

http://h2ham.seesaa.net/article/117267913.html

jAlertやjConfirmの後に$(this)を使うと、想定していたオブジェクトを
指していないようです。
firebugで見たところ、「Window」オブジェクトになっていました。

jAlertやjConfirmの処理の中で$(this)が変わる処理が入っているのでは
ないか、と推測します。

なので、仕方なしに、$(this)で取得したい値をjAlertやjConfirmを使う前に
変数に代入しておくことにします。


[CakePHP]paginateでgroupbyしている時number()が表示されない件

以下のサイトで教えてくれてる対処で表示されるようになりました。
ありがとうございます。

Group Byしている時にpaginator->number()が表示されない件

残った疑問は、オーバーライドしたpaginateCount()だけど、model.php側にも
ないんだね。
どこにも定義されていないメソッドなのに、controller側で呼び出されてるのが
不思議。

2012年12月3日月曜日

[CakePHP]deleteAll()やdelete()の$cascadeはmodel側の設定も必要

$this->Model->deleteAll($conditions,$cascade)の$cascadeに
いくらtrueを設定しても関連モデルのデータを消してくれなくて
調べていたら、 以下のサイトに書いてあった。(英語)

 CakePHP - delete cascade not working

 Model側のアソシエイションを設定するところで、以下のように
dependentをtrueに設定する必要があることがわかった。
 var $hasMany = array(
  'ThemesitemsUser' => array(
   'className' => 'ThemesitemsUser',
   'foreignKey' => 'themesitem_id',
   'dependent' => true,
   'conditions' => '',
   'fields' => '',
   'order' => '',
   'limit' => '',
   'offset' => '',
   'exclusive' => '',
   'finderQuery' => '',
   'counterQuery' => ''
  );
 );

[CakePHP]saveAll()でhasManyのモデルも同時に保存する際の配列の形式

ここに書いてあるように、

3.7.4.1 関連モデル (hasOne, hasMany, belongsTo)のデータを保存する

Model::saveAll()を利用して、hasManyの関係を持つモデルのデータも保存しよう
とする場合、「modelName.0.fieldName」という形になっていないと
だめなんだね。

最初、「modelName.fieldName」っていう風にしていたら、
外部キーに値がセットされなかった。

例えば、Theme hasMany Tagという関係であれば以下のような
配列にしなきゃいけない、ということ。
Array
(
    [Theme] => Array
        (
            [content] => test33です。
            [explanation] => test33です。
        )
    [Tag] => Array
        (
             [0] => Array 
              (
                    [content]=>test
              )
        )
)
ちなみに、上記では[0]のみだが、[1]、[2]....と入れてあげれば、
同時に複数のTagが保存可能だと思われる。(試してないけど)

ちなみに、belongsToの場合は、 「modelName.fieldName」という形式でOK。

2012年12月2日日曜日

[Blogger]blockquoteで引用をかっこよくする。

以下のページを参考にさせて頂きかっこよくできました。

blogger 内の引用をわかりやすくするとか
Google Bloggerの引用を調整する 

<blockquote></blockquote>で囲むと。。。

こんな感じになります。
かっこいいでしょ?
追加したCSSは以下になります。
.post blockquote {
margin: 0px 25px 15px 25px;
padding: 0px 25px 0px 10px;
border: 2px outset #66366a;
background-color: #ece6eb;
}
.post blockquote p {
margin: 0px 0px 0px 0px;
padding: 10px 0px 10px 0px;
}h3.post-title, .comments h4 {
font: normal normal 24px Georgia, Utopia, 'Palatino Linotype', Palatino, serif;
margin: .75em 0 0;
}

[アフィリエイト]楽天アフィリエイトが他のアフィリエイトとの併用可能になった?

※下の追記のところで色々と訂正していますので、最後まで読んで頂きたく。

2ヶ月くらい前に楽天アフィリエイトの規約を読んだ際は
他のアフィリエイトサービスとの併用はだめです、って書いてあった気がしたのだけど、
変わったのか、僕の読み間違えかわからないけど、併用していいみただね。

以下このページhttp://affiliate.rakuten.co.jp/guide/faq/から引用。

Q 他社のアフィリエイトプログラムと同時利用は可能ですか?
A 既に他社のアフィリエイトプログラムを利用されている場合でも、同時に楽天アフィリエイトをご利用になることが可能です。また、同一のページで他社のアフィリエイトプログラムでの広告と楽天アフィリエイトでの広告を掲載することも問題ありません。
ただし、他社のアフィリエイトプログラムの規約によっては、楽天アフィリエイトとの同時利用が禁止されている場合もございますのでご注意ください。

ただ、以下のページより、楽天ブログだと、楽天アフィリエイトのみしか使っちゃだめみたいですね。
http://portal.faq.rakuten.co.jp/app/answers/detail/a_id/4679

 【追記】
2ヶ月前に読んだ規約というのは楽天アフィリエイトではなく、
楽天ウェブサービスの規約だった。  http://webservice.rakuten.co.jp/guide/rule
すみません。

これによると、以下とのこと。

第10条(禁止行為)
(4)ウェブサービスの使用によって楽天アフィリエイト以外の方法で収入を得ること(当社が明示的に許可した場合を除く)
(5)楽天アフィリエイトの成果対象となる商品またはサービス(ウェブサービスによるものを含むがこれに限られない)について、楽天アフィリエイト以外のアフィリエイトプログラムを利用すること(当社が明示的に許可した場合を除く)

がっくし。これによると、やはり楽天ウェブサービスを使った場合は、
楽天以外のアフィリエイトサービス使っちゃだめ、というように理解できる。

でも、よくよく楽天のQAサイト調べてみると以下のような記述があった。
http://webservice.faq.rakuten.co.jp/app/answers/detail/a_id/14259

Q 他社のウェブサービスで取得した商品を併置させることは可能でしょうか?
A 楽天ウェブサービスで取得した商品のページに 他社のウェブサービスで取得した商品を並存させることは問題ありません。(提供元を表示頂ければ)

但し、楽天ウェブサービスを利用して取得した情報を用いて楽天のアフィリエイトリンクを作らずに、他社サービスのアフィリエイトリンクを作り収入を得ることが規約上問題になりますのでご留意お願いします。(規約第10条(5)項)

規約上問題にならない例外が、FAQ#15にありますリンクシェア(LinkShare)のアフィリエイトになります。
あれ、併置させていの?
あっ、そいういうことか。
もしかしたら、自分に都合の良い解釈かもしれないけど、
例えば、楽天APIで取得した商品情報のリンク先として、Amazonのアフィリエイトサイトの
URLを指定してはいけない、ということ。
つまり、逆に言えば、楽天APIで取得した情報には楽天のアフィリエイトURLを指定、
Amazon APIで取得した情報にはAmazonのアフィリエイトURLを指定してあげていれば
一つのサイト内に併置してもOK、ということ。
だったら、こっちにとっても都合がいい。
こういう理解であってるよね?






2012年11月26日月曜日

[CakePHP]recursive=2で取得してしまう不要なデータを取り除く

モデルのアソシエイションで多対多を実現するには、以下の2通りの
方法がある。

model Aとmodl Bが多対多である場合、

1.以下のようにHABTMを設定する。
 ・A HABTM B
 ・B HABTM A

2.belongsToとhasManyの関係に分割する。
 ・A hasMany AsB
 ・AsB belongsTo A,B
 ・B hasMany AsB

1の場合も中間テーブルAsBは必要なのだが、
AとBのidしかカラムに設定できず、そのほかの情報が保存できない、
というデメリットがあるため、私は2を使用する時がある。

そのような場合、Model::find()を使用すると、デフォルトだと中間テーブルまで
しかデータを取得してきてくれない。

なので、recursiveに2を設定する。

ただ、それだと、不要なデータまでたくさん取得してきてしまう可能性がある。
例えば上記の2のアソシエイションと同時に

 ・A belongsTo C
 ・C hasMany A

といったアソシエイションも持っている状況でAがカレントのモデルの際に
A->find()を行うと想定する。

すると、取得してくるデータは以下のようになってしまう。

①A
②A->AsB
③A->AsB->B
④A->AsB->A
⑤A->C
⑥A->C->A

④と⑥は重複している情報であり、無駄である。
なので、この場合は、「AsB belongsTo B」と「C hasMany A」というアソシエイションを
unbindModel()にて一時的に解除してあげればよい。

下記のサイトを参考にさせて頂いた。

CakePHP recursive=2で不要な関係データの取り除き方

2012年11月22日木曜日

[jQuery]コロンを含むセレクタを指定する方法

以下のサイトで説明されているよう、コロンの前に二つのバックスラッシュを
つけてエスケープしなければならない。
なぜ二つかはわからない。

JQueryでコロン付きのセレクターを使う場合

私の場合は既に変数で指定されている文字列を使う必要があったので
下記のようにコロンを置換して実装した。
なお、4行目に試しに置換後の文字列を表示しているが、
なぜか「abc\:1234」とバックスラッシュが一つだけで表示される。
//以下JavaScript部分 
var test ="abc:1234"
test2=test.replace(':',"\\:")
alert(test2);  //「abc\:1234」と表示される。
alert($("#"+test2).val());  //「5678」と表示される。
//以下HTML部分
<input type="hidden" id="abc:1234" value="5678"> 

[CakePHP]セッション上の配列の一番後ろに格納する方法

以下のページの「5.7.1.1 write」にあるよう、CakePHPのsessionコンポーネントの$this->Session->write()で多次元配列を格納できるが、
連想配列でなく通常の配列の一番後ろに格納する方法がわからなかった。

メソッド :: セッション :: 主要なコンポーネント :: マニュアル :: 1.3コレクション 

そこで、以下で紹介されているように

Can I use array_push on a SESSION array in php?
 
$_SESSION['names'] = array();
array_push($_SESSION['names'],$name);
もしくは
$_SESSION['names'][] = $name; 
というようにすれば、namesの下に配列として0から格納し、
最後の添え字に+1した添え字で格納してくれることになる。
なので、シンプルな後者を使うことにした。
ちなみに、0ではない数字(例えば1)からカウントアップしていきたい場合は
以下で紹介されているよう。最初の値を1に格納してしまえばいいそう。

配列について

また、私の場合は、配列の添え字(上の例で言えば'name')
を変数で指定したいと考えていた。
そのためには下記のサイトで説明されているよう変数をダブルクオテーション
で囲む必要があるとのこと。

PHP で、変数を連想配列(ハッシュ)のキーにしたい。

こんな感じ。
$items["$item->id"] = $item->name;
ただ、私の場合は以下のように変数に角括弧を含むものを使おうとしていたら
動かなかった。
$_SESSION["$this->data['TargetSite']"][] = $result;
なので、以下のように一度別の変数に移してから格納したら正常に動いた。
$TargetSite=$this->data['TargetSite'];
$_SESSION["$TargetSite"][] = $result;

【課題】
やはり、 CakePHP標準の$this->Session->write()を使用して
同じことができるようになりたいな。
そちらのほうが、CakePHPの機能のセッション領域をDBにしたりする
のに簡単に対応できそう。
まぁ、ひとまず上記のやり方でいいや。

2012年11月18日日曜日

[CakePHP]error_logを使う際の注意点

ログに配列の中身を表示する際、CakePHPで通常使用するCakeLog::write()だと、
配列の中身は表示できない。
そこで、下記のブログで紹介されているようにerror_logを使用していた。

[php][tips]phpのエラーログに、array配列の中身をきれいに吐く。

ただ、ここで不思議な現象がおきた。

error_logを使用すると、

「Notice (8): Undefined variable」や
「Warning (2): Invalid argument supplied for foreach()」
など、今まで画面に表示されなかったメッセージが画面に表示される
ようになる。
ただ、error_logに渡したメッセージは相変わらずログにしか表示されない。

AJAXの出力でこれが出力されてしまっていて、
動きがおかしくなっていた。
これを防止するには、下記のコマンドでメッセージを出さない
ようにする。
Configure::write('debug', 0);

[CakePHP]update文を発行する保存は同じselect文を二回発行する件について

Model::save()やModel::saveAll()でプライマリーキーがセットされていたら、
自動的にSELECT文を発行して、すでに存在するレコードかどうか確認してくれるんだけど、
なぜか、このSELECT文、まったく同じものが2回発行されるんだよね。

大してユーザがいないシステムなら問題なら良いけど、もし人気サイトなんかに
なったりしたら、アップデートを行う際は、1.5倍のSQL発行量にになってしまうのは
よろしくない気がする。

以下のサイトで対策を教えて下さっているので、
今後、パフォーマンスに問題を感じるようになったらやろう。

CakephpでUPDATEするときのクエリを(少しだけ)減らしてみた

2012年11月6日火曜日

[CakePHP]scriptBlock()やscriptStart()をechoする必要がある場合。

CakePHPのHtmlHelper->scriptBlock()やscriptStart()は$scripts_for_layoutで出力されない

 このサイトに書いてあるように、'inline' => trueで使用する場合は、
echoしてあげなっきゃスクリプトがはかれない。
少しはまった。 

[Ajax]Ajaxで取得したHTML内のスクリプトを実行するには

このページに書いてあるよう、scriptタグを指定して、evalをかければ実行できた。

Ajax/Ajaxで取ってきたHTMLコンテンツ内の<script>タグを実行させたい

[jQuery]$.ajaxではまった件

$.ajax({
    complete:function (XMLHttpRequest, textStatus) {
        document.getElementById("result").innerHTML=XMLHttpRequest.responseText;
    },
    data:"data[Keywords]="+$("#lastKeywords").val()+"&data[ItemPage]="+$("#nextPage").val();
    type:"post",
    url:    "\/rakuzon\/themes\/itemSearch"
});
このコードが動かなかったんだけど、5行目のセミコロンをカンマにしてあげたら
動いた。(丸一日つぶした。。。)
また、dataには配列として渡してあげたほうが見やすいきがしたので以下のように修正。
$.ajax({
    complete:function (XMLHttpRequest, textStatus) {
        document.getElementById("result").innerHTML=XMLHttpRequest.responseText;
        eval($("#result script").text());
    },
    data:{
        'data[Keywords]':$("#lastKeywords").val(),
        'data[ItemPage]':$("#nextPage").val()
    },
    type:"post",
    url:"\/rakuzon\/themes\/itemSearch"
}); 
以下が参考にしたサイト。

[jQuery]postメソッドとajaxメソッドを使ってPOST送信を行うサンプル
連想配列の使用方法

【追記 2012/11/6】
なお、data:に設定するのは連想配列となる。
事前に連想配列を以下のように用意しておくことも可能だ。
postData={
    'data[Keywords]':$("#lastKeywords").val(),
    'data[ItemPage]':$("#prevPage").val()
};
$.ajax({
    complete:function (XMLHttpRequest, textStatus) {
        $("#result").html(XMLHttpRequest.responseText);
        bindPager();
    },
    data:postData,
    type:"post",
    url:"\/rakuzon\/themes\/itemSearch"
});
しかし、以下のように連想配列の要素ごとに代入していくやり方だと
なぜかうまく$.ajax()が動かなかった。原因不明。
postData['data[Keywords]']=$("#lastKeywords").val();
postData['data[ItemPage]']=$("#prevPage").val();
$.ajax({
    complete:function (XMLHttpRequest, textStatus) {
        $("#result").html(XMLHttpRequest.responseText);
        bindPager();
    },
    data:postData,
    type:"post",
    url:"\/rakuzon\/themes\/itemSearch"
}); 

2012年11月5日月曜日

[PHP]xdebugで変数の値が変化しない場合

下記のサイトで紹介されているバージョンのxdebugを 導入したら変化するようになった。
 自分が使っているのはおそらくもう少し古いやつだ。

eclipseでphpのデバッグをすると変数の値がになる~xdebug

2012年11月3日土曜日

[CakePHP]一気に複数のアソシエイションをunbindModelする

単純にそのモデルだけにfindしたいだけなのに、
アソシエイション先のモデルも引っ張ってきてしまうのは
負荷の問題上よろしくない気がする。

今までは全てのアソシエイションを指定してunbindModelをしていたのだが、
以下の記事によると、recursiveを設定したほうが手軽ということがわかった。

まとめてunbindModelするとき

ただし、設定の仕方は、
$params = array(‘recursive’ => -1);
よりも
 $this->Model->recursive = -1;

のほうが良い気がします。

[JavaScript]処理の途中で強制終了させるには。

exit、returnのようなコマンドで処理を強制終了させることができる
プログラミング言語って結構あるけど、JavaScriptはそういうコマンドが
ないとのこと。

なので、try~cacth構文を使用して代替する。
以下のサイトがわかりやすい。

JavaScript try catchによる例外処理

2012年11月2日金曜日

[CakePHP]$usesの謎

以下のようなアソシエイションの関係がある場合。

Theme hasAndBelongsToMany Tag
Tag hasAndBelongsToMany Theme
Tag hasMany ThemesTag
Theme hasMany ThemesTag
ThemesTag belongsTo  Theme
ThemesTag belongsTo Tag

 MainControllerというどのモデルの名前も使わない
コントローラーにて、
public $uses = array('Item','Tag','Theme','ThemesTag');

というように使用するモデルを指定した場合、問題が一つ起きる。

以下のような、HABTM関係の中間モデル、ThemesTagでのpaginate(もしかしたらfindも)にて
アソシエイション先のテーブルからデータをとってこない、という問題。
上記のアソシエイションの関係では、belongsToの関係を持つ、
ThemeとTagをJOINして持ってくると期待していたのに。
また、ThemeやTagの場合は期待通りアソシエイション先からデータを取得してくれる。
 $this->paginate('ThemesTag');

そこで、ためしに以下のように$usesで指定する順番を変えてみる。
 public $uses = array('ThemesTag','Item','Tag','Theme);

このように1番目にThemesTagを持ってくると期待通り、
アソシエイション先のテーブルからデータを取得してく れた。

次に、以下のようにThemesTagを2番目に指定したところ、
やはりだめだった。
 public $uses = array('Item','ThemesTag','Tag','Theme);

続いて、以下のようにpaginateの直前でbindModelを行ったところ、
アソシエイション先のテーブルからデータを取得してく れた。
$this->ThemesTag->bindModel(array('belongsTo'=>array('Theme','Tag')),false);

 【推論】
■中間テーブルは自モデルのコントローラでのみ、
 bindModel無しでアソシエイション先のテーブルからデータを取得する。
 ※自モデルのコントローラとは上記の例でいえば
     ・ThemesTagController。
     ・nameにThemesTagを指定したController 。
     ・どうやら以下のサイトによると$usesの先頭で指定したモデルが自モデルとなるらしい。
   Controller $uses への Model 名の登録順序
 

【結局どうするか】
$nameで中間テーブルを指定するのも変だし、
$usesで1番目を指定するというトリッキーなこともしたくないので、
bindModelをすることにする。

でも、なんで中間テーブルだけ違うのか謎だよなぁ。



2012年10月26日金曜日

[jQuery]かっこいいアラートやプロンプトを使う。

このページを見てできました。
なかなか素敵。

ダイアログボックスを美しくみせる「jQuery Alert Dialogs」を使ってみる


別のやつで下記もいいかなと思ったんだけど、
なんか表示されるフェードインが遅すぎるんだよな。

jQuery Impromptu | 設置サンプル

[JavaAcript]外部ファイル化する際はonloadする必要がある

下記のページにあるよう、JavaScriptを外部ファイル化する際は、
外部化するスクリプト全体をwindwo.onloadで指定しなければならない。
ただ、これをしてしまえば、htmlのほうにJavaScriptを書く必要はない。

JavaScript の基本

2012年10月25日木曜日

[CakePHP]外部化したcssやjsをコントローラで出力する。

なるほどね。
cssやjavascriptを外部ファイル化してhtmlと切り離すのは、
SEO対策や読み込み速度といった面でメリットがあるのだけど、
ファイルパス等、開発途中で変更したくなる値を動的に変更できなく
なってしまうので、いちいち全部修正しなくてはならない、というデメリット
がある。

そこで、controllerで出力させてしまえば、自動的に値を変動させる
ことができる。

以下が参考。

[CakePHP] スタイルシートをコントローラーで作ってみる

2012年10月16日火曜日

[jQuery]bindについてわかったこと

・イベントによって生成された要素にbind()するには。 


生のHTMLに書かれた要素に対してイベント登録するには
単純にbind()すればよい。
だが、イベント発生後にJavaScriptで生成された要素に対してイベント登録するには
要素の作成時にbindしなくてはならない。

$("#submit-1528237246").bind("click", function (event) {
                $("#tags").append("<p><span class=\"tag\">"+$("#tag").val()+"</span> <span class=\"rm\">×</span></p>");
                rmTag();
});

function rmTag(){
    $(".rm").unbind();
    $(".rm").bind({
        click:function(event){
            $(this).parent().remove();
        },
        mouseenter:function(){
            $(this).css("cursor","pointer");
        },
        mouseleave:function(){
            $(this).css("cursor","default");
        }
    });
}
2行目によってクリック時に要素を追加する設定をしている。
ここで追加された要素に対してイベント登録するには、この作成時の時点で
イベント登録を行わなければならない。
なので3行目でイベント登録する関数を呼び出している。

・bindによるイベント登録は上書きではなくappend


ちなみに、7行目でunbindしているのは下記のサイトで説明されているよう
bindは上書きではなくappendしていくためだ。

jQueryのイベント時の関数を上書きする

上記のソースのようにイベント発生時にrmクラス全てにイベント登録して
いくと、最初の方にbindされた要素は同じイベント登録が複数回appendされて
しまうため、毎回全登録をunbindしている。

・一つの要素に複数のイベント登録をするには


また、一つの要素に複数のイベント登録をするには8~18行目のようにする。

・"hover"はbind()で使用できない。


 下記のようにhover()という関数を使ってイベント登録をできるが、
これをbind()で実現するには上記のソースのように、mouseenterとmouseleaveに
分けて記述する必要がある。

            $(function(){
                $(".rm").hover(
                    function(){
                        $(this).css("cursor","pointer"); 
                    },
                    function(){
                        $(this).css("cursor","default"); 
                    }
                );
                $(".rm").click(
                    function(){
                        $(this).parent().remove();
                    }
                );
            });


[eclipse][SVN]subversiveで削除コミットしてしまったファイルを戻す

eclipse×subversiveでSVN上から削除コミットしてしまったファイルを
以前のリビジョンから戻す手順を示す。


1.SVNリポジトリエクスプローラにて下記のようにヒストリを表示する。






2.削除コミットをする直近で対象のファイルに変更をしたリビジョン番号をクリックする。
3.下の画面の一覧に削除したファイルが表示されるので右クリックし、「改定リンクの追加」
  をクリックする。



4.すると下記のように改定リンクが表示されるので、リファクタリング→コピーをクリック。


5.戻す先のディレクトリを選択し、次へを押す。


6.コミットする。




7.PHP(Java)エクスプローラーに戻って、「チーム」→「更新」をクリックすると、対象ファイルが
 表示される。

8.リポジトリエクスプローラーに戻り、改定リンクを削除する。




2012年10月15日月曜日

[Error記録]"Sorry, that page does not exist","code":34

Twitter OAuth認証時に新たなErrorが出た。

ブラウザ上ではこのようなError。
5~11行目を見るとわかるように、requestTokenとaccessTokenの取得には成功しているのに、
なぜか、11行目でTwitter側から情報が取得できず、 "Sorry, that page does not exist","code":34
 というメッセージが返ってきている。
ただ、この状態で、別画面に遷移してみると、認証されてユーザ情報が表示された。
ログイン時、ユーザ情報をTwitter側から更新する仕組みになっているが、
更新は行われず、取得したaccessTokenでログインのみした状態となったと思われる。


Notice (8): Undefined index: id_str [APP\controllers\users_controller.php, line 66]Code | Context            )));
        }
        $output = call_user_func_array(array(&$controller, $params['action']), $params['pass']);$consumer    =    OAuth_Consumer

$requestToken    =    OAuthToken
OAuthToken::$key = "XXXXXXXXXXXXXXXXXXXXX"
OAuthToken::$secret = "XXXXXXXXXXXXXXXXXXXXX"
$accessToken    =    OAuthToken
OAuthToken::$key = "XXXXXXXXXXXXXXXXXXXXX"
OAuthToken::$secret = "XXXXXXXXXXXXXXXXXXXXX"
$json    =    "{"errors":[{"message":"Sorry, that page does not exist","code":34}]}"
$twitterData    =    array(
    "errors" => array(
    array()
)
)UsersController::twtrCallback() - APP\controllers\users_controller.php, line 66
Dispatcher::_invoke() - CORE\cake\dispatcher.php, line 204
Dispatcher::dispatch() - CORE\cake\dispatcher.php, line 171
[main] - APP\webroot\index.php, line 86
Notice (8): Undefined index: screen_name [APP\controllers\users_controller.php, line 67]Code | Context            )));
        }
        $output = call_user_func_array(array(&$controller, $params['action']), $params['pass']);$consumer    =    OAuth_Consumer

$requestToken    =    OAuthToken
OAuthToken::$key = "XXXXXXXXXXXXXXXXXXXXX"
OAuthToken::$secret = "XXXXXXXXXXXXXXXXXXXXX"
$accessToken    =    OAuthToken
OAuthToken::$key = "XXXXXXXXXXXXXXXXXXXXX"
OAuthToken::$secret = "XXXXXXXXXXXXXXXXXXXXX"
$json    =    "{"errors":[{"message":"Sorry, that page does not exist","code":34}]}"
$twitterData    =    array(
    "errors" => array(
    array()
)
)UsersController::twtrCallback() - APP\controllers\users_controller.php, line 67
Dispatcher::_invoke() - CORE\cake\dispatcher.php, line 204
Dispatcher::dispatch() - CORE\cake\dispatcher.php, line 171
[main] - APP\webroot\index.php, line 86
Notice (8): Undefined index: profile_image_url [APP\controllers\users_controller.php, line 70]Code | Context            )));
        }
        $output = call_user_func_array(array(&$controller, $params['action']), $params['pass']);$consumer    =    OAuth_Consumer

$requestToken    =    OAuthToken
OAuthToken::$key = "XXXXXXXXXXXXXXXXXXXXX"
OAuthToken::$secret = "XXXXXXXXXXXXXXXXXXXXX"
$accessToken    =    OAuthToken
OAuthToken::$key = "XXXXXXXXXXXXXXXXXXXXX"
OAuthToken::$secret = "XXXXXXXXXXXXXXXXXXXXX"
$json    =    "{"errors":[{"message":"Sorry, that page does not exist","code":34}]}"
$twitterData    =    array(
    "errors" => array(
    array()
)
)UsersController::twtrCallback() - APP\controllers\users_controller.php, line 70
Dispatcher::_invoke() - CORE\cake\dispatcher.php, line 204
Dispatcher::dispatch() - CORE\cake\dispatcher.php, line 171
[main] - APP\webroot\index.php, line 86
Warning (512): SQL Error: 1048: Column 'user_id' cannot be null [CORE\cake\libs\model\datasources\dbo_source.php, line 684]Code | Context
        if ($this->error) {
            $this->showQuery($sql);$sql    =    "INSERT INTO `users` (`user_id`, `user_name`, `access_token_key`, `access_token_secret`, `img_url`, `updated`, `created`) VALUES (NULL, NULL, 'XXXXXXXXXXXXXXXXXXXXX', 'XXXXXXXXXXXXXXXXXXXXX', NULL, '2012-10-15 16:15:39', '2012-10-15 16:15:39')"
$error    =    "1048: Column 'user_id' cannot be null"
$out    =    nullDboSource::showQuery() - CORE\cake\libs\model\datasources\dbo_source.php, line 684
DboSource::execute() - CORE\cake\libs\model\datasources\dbo_source.php, line 266
DboSource::create() - CORE\cake\libs\model\datasources\dbo_source.php, line 750
Model::save() - CORE\cake\libs\model\model.php, line 1342
User::update() - APP\models\user.php, line 52
UsersController::twtrCallback() - APP\controllers\users_controller.php, line 72
Dispatcher::_invoke() - CORE\cake\dispatcher.php, line 204
Dispatcher::dispatch() - CORE\cake\dispatcher.php, line 171
[main] - APP\webroot\index.php, line 86
Query: INSERT INTO `users` (`user_id`, `user_name`, `access_token_key`, `access_token_secret`, `img_url`, `updated`, `created`) VALUES (NULL, NULL, 'XXXXXXXXXXXXXXXXXXXXX', 'XXXXXXXXXXXXXXXXXXXXX', NULL, '2012-10-15 16:15:39', '2012-10-15 16:15:39') 
Warning (2): Cannot modify header information - headers already sent by (output started at C:\xampp\htdocs\rakuzon\cake\libs\debugger.php:686) [CORE\cake\libs\controller\controller.php, line 742]Code | Context */
    function header($status) {
        header($status);$status    =    "Location: http://127.0.0.1/rakuzon/main/index"header - [internal], line ??
Controller::header() - CORE\cake\libs\controller\controller.php, line 742
Controller::redirect() - CORE\cake\libs\controller\controller.php, line 721
UsersController::twtrCallback() - APP\controllers\users_controller.php, line 82
Dispatcher::_invoke() - CORE\cake\dispatcher.php, line 204
Dispatcher::dispatch() - CORE\cake\dispatcher.php, line 171
[main] - APP\webroot\index.php, line 86 
https://dev.twitter.com/discussions/11595
ここに対処策が出ていた。 ユーザ情報を引き出す際のURLを以下のように変更したら正常に動作した。
http://twitter.com/account/verify_credentials.json
 ↓
http://api.twitter.com/1/account/verify_credentials.json

また、以下のページによると
https://dev.twitter.com/discussions/10803 
現在以下を使用できないように変更中のようだ。
  • All API endpoints on www.twitter.com and twitter.com.
  • Endpoints on api.twitter.com without /1, /1.1 or /oauth in the fully qualified URL.
私の場合、twitter.comでアクセスし、1や1.1等のバージョンも無かったのでだめだった。
ただ、ソースの他の部分でまだtwitter.comで動いているところもあるので、
バージョンが無いのがダメだったということだろう。
twitter.comのままの部分は直さないと
今後またおかしなエラーが発生してしまうかもしれない。

2012年10月10日水曜日

[CakePHP]あるControllerから別のモデルにアクセスする場合

アソシエイションを設定している場合は、例えば以下のような感じでアクセスできるみたい。
Theme hasAndBelongsToMany Tagの関係を持ち、
ThemesControllerから、Tagのメソッドを使用したい場合。
$this->Theme->Tag->newSave($this->data['Tag']);
特にTagモデルのロード等は行っていない。 これはすごく基本かもしれないけど。

※CakePHPのバージョンはcakephp-cakephp-1.3.15-9-gacd25c3.zip

[CakePHP]$js->submit()のoptionsのbeforeの限界と対策

以下のような形でbeforeオプションを使用し、
ajax送信直前に送ろうとしているformの値自体を操作しようと
したのだけど、うまく送れなかった。
おそらく、beforeが完了してから送信の処理が開始されるので
はなく、beforeと同時並行的に送信するデータ自体は取得して
いるからだろう。

echo $js->submit("登録する",array(
    'id'=>'register',
    'url'=>'add',
    'update'=>'#complete'
    'before'=>'
        var fm = document.getElementById("add");
        $("#add").append("<input type=\"hidden\" name=\"data[Theme][explanation]\"/>");
        fm.elements[\'data[Theme][explanation]\'].value=$("#explanation").val();
    '
));

では、どうすればよいか。
beforeで設定するのではなく、別で$js->submitより上に、
onclickのイベント設定をしてしまえばよい。
こうすれば、フォームの値の操作が完了してから、
送信の処理が開始されるようになる。

$js->get("#register")->event('click','
    var fm = document.getElementById("add");
    $("#add").append("<input type=\"hidden\" name=\"data[Theme][explanation]\"/>");
    fm.elements[\'data[Theme][explanation]\'].value=$("#explanation").val();
');

echo $js->submit("登録する",array(
    'id'=>'register',
    'url'=>'add',
    'update'=>'#complete'
)); 
※CakePHPのバージョンはcakephp-cakephp-1.3.15-9-gacd25c3.zip

[PHP]PHPでJavaScriptを書き出す際のクオテーションについて

PHPでjavascriptを吐き出す場合、シングルクオテーションで
囲っている例が多い。もしかしたらダブルでもよいのかもしれないが、
シングルで囲う場合を考える。

以下phpのスクリプトで2~6行目がJavaScriptである。

$js->get("#register")->event('click','
    var fm = document.getElementById("add");
    $("#tags span").each(function(i){
        $("#add").append("<input type=\"hidden\" name=\"data[Tag]["+i+"][content]\"/>" );
        fm.elements[\'data[Tag][\'+i+\'][content]\'].value=$(this).text();
     });
');

javascript全体がシングルでかこわれている。
この中でさらにクオテーションを使用したい場合、
以下のサイトから、

PHPで javascriptを書き出すときのクォーテーションの使い方

「PHPとしての最初のクォティションと最後のクォティションはそのままで、
内側の同種クォティションはエスケープが基本」

らしいです。

なので、私も

・ダブルはそのまま使う。
・シングルを使いたい場合はエスケープ。

ただし、

javascriptが吐き出す文字列にダブルクオテーションを
付与したい場合は、エスケープしないとだめなようだ。

[CakePHP]inputタグのnameをjQueryで指定できない。

CakePHPでajaxを使ってフォームの内容を送信する際に、
javascriptでinputタグのtype=hiddenのvalueを操作したい場合が
あったのですが、その際に対象のinputエレメントを指定するのに
苦労しました。

inputタグにidをつけてしまえば簡単なのですが、
nameというユニークの属性を持っておきながら、
さらにidを付与するのは少し不細工なので、なんとか
nameで指定するやり方を考えました。

jQueryだと通常、
$(*"input[name=XXX]")
といった形で、name属性で指定できる。

だが、CakePHPのhtmlヘルパーで作成されるinputタグは以下のように
"["と"]"を含むので指定することができない。
色々な方法でエスケープを試みたができなかった。
<input type="text” name=”data[Model][field]“/> 
なので、以下のサイトを参考にした。

cakePHPの Formヘルパーで作られるdata[Model][Field]をJavascript処理する
[CakePHP] 配列のname属性をJavaScriptで参照する方法
var fm = document.getElementById("add");
//中略
fm.elements[\'data[Item][\'+i+\'][code]\'].value=itemLst[i];
上記のような形でnameを変数で動的に変動させることもできるのだ。
上記の例では、formエレメントをidで取得してきているが、
nameをつけている場合には、以下のように書くことができる。
(formのnameが"add"である場合)
document.add.elements[\'data[Item][\'+i+\'][code]\'].value=itemLst[i]; 
※ここでのバックスラッシュはWindwosでいういわゆるYENマークね。
※CakePHPのバージョンはcakephp-cakephp-1.3.15-9-gacd25c3.zip

[CakePHP]HABTMでのデータ保存について(両モデル新規登録・複数行登録)

カレントモデルと同時にHABTM(hasAndBelongsToMany)の関係を持つ
モデルのデータを同時に保存したい場合、色々の問題があり、
それに対して検討した結果を紹介する。

以下で紹介したように、HABTMしているモデルのデータは
idの配列となっている必要がある。

[CakePHP]アソシエイションを持つモデルをsaveall()する際に受け付けるデータ構造

つまり、view側にて、idが取得できる状態が前提となっている
ようだ。なので、id以外の項目のみを送信しても、普通には保存できない。
ただ、一般にウェブサービスで使われている「タグ」などの機能は、
ユーザがその画面でタグ自体を作成することが想定される。
その場合、カレントモデルとHABTMの関係を持つモデルの両方を
新規登録する必要がある。

その場合、どのように実現するか。
いくつかの方法を検証してみた。

1.中間テーブルのsaveallメソッドを使用する。

 

3.7.6.6 hasMany through (The Join Model)

cookbookで紹介されているこのやり方では、
実際はHABTMを利用しているわけではなく、中間テーブルからの
belongsToを利用した保存方法だ。
この方法だと、以下のようにid以外の項目での保存が可能だ。
両方のモデルにidをつけてくれるし、中間テーブルにも
そのidでレコードを挿入してくれる。

※Theme habtm Tagという関係を想定

配列構造①

Array
(
    [Theme] => Array
        (
            [content] => test33です。
            [explanation] => test33です。
        )
    [Tag] => Array
        (
            [content] => 村田さん
        )
)

一つ注意点があり、Themeがカレントだと、なぜか、
ThemesTagからのbelongsToのアソシエイションをbindしなければ
ならない。ThemesTagモデルでアソシエイションを設定したとしても。
でないと、ThemesTagに空のレコードを挿入するのみとなってしまう。
以下のサイトを参考にした。

Habtm | habtm な関係にあるモデルで、両方とも未登録なデータを同時に保存したい

複数レコードの挿入はできるだろうか。
まず以下のような配列構造にしてみた。
結果Tagが保存されなかった。

配列構造②

Array
(
    [Theme] => Array
        (
            [content] => test33です。
            [explanation] => test33です。
        )
    [Tag] => Array
        (
             0=>array('content'=>'村田さん')
             1=>array('content'=>'児島さん'),
             2=>array('content'=>'本'),
        )
)

次に以下のような配列構造にしてみた。
こちらは、ThemesTagsに2行の空行(createdとupdatedは入った)だけ
が挿入されたのみだった。

配列構造③
Array
(
    [1] => Array
        (
            [Theme] => Array
                (
                    [content] => test34です。
                    [explanation] => test34です。
                )
            [Tag] => Array
                (
                    [content] => 村田さん
                )
        )
    [2] => Array
        (
            [Theme] => Array
                (
                    [content] => test34です。
                    [explanation] => test34です。
                )
            [Tag] => Array
                (
                    [content] => 児島さん
                )
        )
)

次に2回に分けてsaveallしてみたらどうかと考え以下のようにしてみた。

コード例①

        $data=array(
                'Theme'=>array(
                    'content'=>'test35です。',
                    'explanation'=>'test35です。'
                ),
                'Tag'=>array(
                    'content'=>'村田さん'
                )
         );
        $data2=array(
                'Theme'=>array(
                ),
                'Tag'=>array(
                    'content'=>'児島さん'
               )
         );
        $this->Theme->saveall($data);
        $data2['Theme']['id']=$this->Theme->id;
        $this->Theme->saveall($data2);

すると、見事に保存することができた。以下のようなSQLが発行された。

1    SHOW FULL COLUMNS FROM `themes`        6    6    4
2    SELECT CHARACTER_SET_NAME FROM INFORMATION_SCHEMA.COLLATIONS WHERE COLLATION_NAME= 'utf8_general_ci';        1    1    1
3    SHOW FULL COLUMNS FROM `users`        8    8    3
4    SHOW FULL COLUMNS FROM `items`        6    6    3
5    SHOW FULL COLUMNS FROM `tags`        4    4    3
6    SHOW FULL COLUMNS FROM `themes_tags`        5    5    3
7    START TRANSACTION        0        0
8    INSERT INTO `themes` (`content`, `explanation`, `modified`, `created`) VALUES ('test35です。', 'test35です。', '2012-10-10', '2012-10-10')        1        0
9    SELECT LAST_INSERT_ID() AS insertID        1    1    0
10    INSERT INTO `tags` (`content`, `modified`, `created`) VALUES ('村田さん', '2012-10-10', '2012-10-10')        1        0
11    SELECT LAST_INSERT_ID() AS insertID        1    1    0
12    INSERT INTO `themes_tags` (`theme_id`, `tag_id`, `modified`, `created`) VALUES (3879, 78, '2012-10-10', '2012-10-10')        1        0
13    SELECT LAST_INSERT_ID() AS insertID        1    1    0
14    COMMIT        0        0
15    START TRANSACTION        0        0
16    SELECT COUNT(*) AS `count` FROM `themes` AS `Theme` WHERE `Theme`.`id` = 3879         1    1    0
17    SELECT COUNT(*) AS `count` FROM `themes` AS `Theme` WHERE `Theme`.`id` = 3879         1    1    0
18    UPDATE `themes` SET `id` = 3879, `modified` = '2012-10-10' WHERE `themes`.`id` = 3879        0        0
19    INSERT INTO `tags` (`content`, `modified`, `created`) VALUES ('児島さん', '2012-10-10', '2012-10-10')        1        0
20    SELECT LAST_INSERT_ID() AS insertID        1    1    0
21    INSERT INTO `themes_tags` (`theme_id`, `tag_id`, `modified`, `created`) VALUES (3879, 79, '2012-10-10', '2012-10-10')        1        0
22    SELECT LAST_INSERT_ID() AS insertID        1    1    0
23    COMMIT

for文で回せばいくつでも保存できそうだ。
ただし、これは、トランザクションが二つにわかれてしまっているが。

ただ、この方法のデメリットとしては、同じ内容のTagを作ってしまう
可能性があるので、それを回避する処理を入れる必要があるという点。

また、以下で議論されているよう連続した保存処理の場合、
Model::create()を毎回実行しないと最後の行しか反映されない、
と思っていたが、このやり方だと、create()しなくてもきちんと保存されていた。
中間テーブルのsaveallを使用しているからだろうか。

CakePHPのsaveメソッドでINSERTするつもりがUPDATEになってしまう場合
連続したsaveメソッドの使い方について

2.habtamable.phpを使用する。


こちらは有志の方が作ったbehaviorだ。
以下でダウンロード可能。

https://github.com/teknoid/cakephp-habtamable-behavior/blob/master/models/behaviors/habtamable.php

使い方はダウロードしたフォルダに英語のreadmeが入っている。

こちらは配列構造①の形で保存可能。

また、その場合、「村田さん」というタグがすでに
存在する場合は、Tagsテーブルに新規レコードを追加せず、
既存のレコードのidを取得し、中間テーブルに挿入してくれる。
また、メリットとして、Themeのsaveall()を使うため、
以下のように、Themeがもつほかのアソシエイションモデルのデータも
同時に保存することができる。

こちらは
Theme hasMany Item
Theme hasAndBelongsToMany Tag
というアソシエイションを持つ場合。

配列構造④

Array
(
    [Theme] => Array
        (
            [content] => test33です。
            [explanation] => test33です。
        )
    [Item] => Array
        (
            [0] => Array
                (
                    [code] => B0017LURGI
                    [point] => 10
                )
            [1] => Array
                (
                    [code] => 4774135038
                    [point] => 9
                )
    [Tag] => Array
        (
            [content] => 村田さん
        )
)

では、複数レコードの保存についてはどうだろうか。
配列構造②のようにしたところ、Tagの保存ができなかった。
配列構造③のようにしたところ、一つ目のデータはきちんと
保存できたのだが、二つ目のデータは空行などのおかしなデータ
しか入らなかった。

コード例①のようにしたところ、ThemesとTagsに対するレコード挿入は
適切に行われたのだが、ThemesTagの2行目を挿入する直前に1行目を
削除してしまうという現象が起きた。

こちらは2度目のsaveall直前で以下のそれぞれを試してもダメだった。

①$this->Theme->create();
②$this->Theme->create(false);
③$this->Theme->create(null);
④$this->Theme->id=null;
⑤$this->Theme->ThemesTag->create();
⑥$this->Theme->ThemesTag->create(false);
⑦$this->Theme->ThemesTag->create(null);
⑧$this->Theme->ThemesTag->id=null;
⑨$this->Theme->ThemesTag->Theme_id=null;


【まとめ】
1.中間テーブルのsaveallメソッドを使用する。
 メリット:
  ・id以外のデータで登録可能
  ・両モデルの新規登録可能
  ・saveallを複数回に分けることにより複数行の挿入が可能。

 デメリット:
  ・複数行挿入する場合、トランザクションが行数分となる。
  ・同じデータがあっても新しい行を挿入してしまう。(回避策をたてる必要あり)

2.habtamable.phpを使用する。
 メリット:
  ・id以外のデータで登録可能
  ・両モデルの新規登録可能
  ・同じデータがある場合は、既存のレコードのIDを使用してくれる。
  ・他の他のアソシエイションを持つモデルのデータも同時に保存できる。

 デメリット:
  ・複数行の登録ができない。


【結局どうしたか】
上記で紹介した二つとも、どっちもどっち、と思ったので、
通常のhabtmの保存方法、idでの保存で、以下を実現する方法を考える
ことにした。
  ・両モデルの新規登録可能
  ・同じデータがある場合は、既存のレコードのIDを使用してくれる。

やり方としては、(Tagが対象モデルと想定)
①viewからはid以外の情報が送られるようにする。
 (新規も既存も混在)

②それをcontroller側で、idの配列に変換する。
 (既存に同じデータがあるものは既存のid、無いものは
  そこでTagsテーブルにsaveし、idを取得する。)

③それをsaveallに渡すデータ配列に組み込む。
 (最終的に以下のような配列構造となるようにする。)

Array
(
    [Theme] => Array
        (
            [content] => 本
            [explanation] => 楽しい本
            [user_id] => 4
        )
    [Tag] => Array
        (
            [0] => 2
            [1] => 68
        )
)

このやり方だと、もし、新規のTagが発生しない場合は、
トランザクションは1回で済むというメリットがある。
新規のTagがある場合は、その数+1回のトランザクションとなる。
「1.中間テーブルのsaveallメソッドを使用する。」のやり方の
場合は、新規があろうとなかろうと、Tag数分のトランザクションが
発生してしまうし、他のアソシエイションのデータについても別トランザクション
で保存する必要があるので、こちらのほうが良いと考えた。

また、CakePHPで通常想定されるのhabtmの保存方法に近いほう
がよいかとも思ったので。

※CakePHPのバージョンはcakephp-cakephp-1.3.15-9-gacd25c3.zip


【追加 2012/11/17】
この記事で何度かトランザクションが分かれてしまうことを問題にして
いるが、下記の記事にあるようbegin()とcommit()で解決できそうだ。
つまり、一つのトランザクションにできそう。
ただ、下記のブログで工夫している、複数のモデルを使うとわかりにくくなる
ことへの対応まではしていない。

CakePHPで複数テーブルに対するトランザクションを使う場合

[CakePHP]saveall()は一段階のアソシエイションモデルしか保存できない。(v1.3)

saveall()にてアソシエイションの関係を持つモデルの情報も
同時に保存できるが、アソシエイションを持つモデルがアソシエイションを
持つモデルは保存できない。

以下のサイトでわかった。
saveAll には4階層までの制限がある?

またどこかの英語のサイトでversion2.0か2.1では、saveall()でも
recursionが有効で、何段階先のアソシエイションでも保存できる
と書いてあった。

※CakePHPのバージョンはcakephp-cakephp-1.3.15-9-gacd25c3.zip

[CakePHP]アソシエイションを持つモデルをsaveall()する際に受け付けるデータ構造

あるモデルのデータを保存する際、Model::saveall()を使用すれば、
アソシエイションを持つモデルのデータも同時に保存できるのは大変便利だ。

ただし、saveall()の引数に渡すデータの配列構造に決まりがあるので、
それをおさえなくてはならない。

view側のformから送信されたデータはcontroller側では、$this->dataで
取得することができる。
大抵これをそのままsave()やsaveall()に
$this->Model->saveall($this->data);
といった形で渡し保存することができる。

ただ、view側のフォームで適切にデータを構成した場合は良いが、
色々な事情で適切な形でデータを送れない場合がある。
そういった場合は、controller側でデータを構成しなおしてあげて
saveall()に渡してあげればよい。
逆に言えば、その構成にさえしてあげれば、view側からどのようにデータを
送信されても保存ができるということだ。

その「適切なデータ構成」をわかっている範囲で紹介する。
以下はThemeControllerから、$this->Theme->saveall($data);する際の
$dataの配列構造を想定している。

・hasMany


以下はTheme hasMany Itemという関係を持つ場合である。
Itemが複数となっている。

Array
(
    [Theme] => Array
        (
            [content] => 本
            [explanation] => 楽しい本
            [user_id] => 4
        )

    [Item] => Array
        (
            [0] => Array
                (
                    [code] => B0017LURGI
                    [point] => 10
                )

            [1] => Array
                (
                    [code] => 4774135038
                    [point] => 9
                )
         )
) 


これを保存すれば、自動的にItemモデルのレコードに
Themeのidを補填してくれる。

・HABTM(hasAndBelongsToMany)


Theme HABTM Tagの関係である。

Array
(
    [Theme] => Array
        (
            [content] => 本
            [explanation] => 楽しい本
            [user_id] => 4
        )

    [Tag] => Array
        (
            [0] => 2
            [1] => 68
        )

)


Tagのほうは、プライマリーキーのidの配列である。(これが超重要)
※連想配列ではなく、ただの配列。
このデータをsaveall()で保存すれば、自動的にThemesTagという
中間テーブルにも両モデルのidを組み合わせたレコードを挿入してくれる。

・hasManyとHABTMの両方を持つ場合。


Theme hasMany Item
Theme HABTM Tag
という関係を持つ場合は、上記で紹介した配列を合体させれば
よいだけとなる。

Array
(
    [Theme] => Array
        (
            [content] => 本
            [explanation] => 楽しい本
            [user_id] => 4
        )

    [Item] => Array
        (
            [0] => Array
                (
                    [code] => B0017LURGI
                    [point] => 10
                )

            [1] => Array
                (
                    [code] => 4774135038
                    [point] => 9
                )

        )

    [Tag] => Array
        (
            [0] => 2
            [1] => 68
        )
)



【課題】
上記のHABTMのモデルのデータ構成は
data[Tag]=array(2,68)
といった形でデータが入っているが、以下のサイトだと
data[Tag][Tag]=array(2,68)
といった形にしないといけないように読める。
どっちでもいけるってことなのかな。
暇な時に試します。

cakePHPの$model->saveにおけるUpdateとInsert

※CakePHPのバージョンはcakephp-cakephp-1.3.15-9-gacd25c3.zip

2012年10月5日金曜日

[CakePHP]jsヘルパーのサブミットを使用するならformタグでaction指定する必要ない。

下記のようにsubmitボタンをjsヘルパーで作成していてurlも指定している場合は、 4行目のactionの指定をしなくても動いた。
    <?php
        echo $form->create(false,array(
            'type'=>'post'
/*            'action'=>'mkHtml'*/
        ));
        echo "商品URL:".$form->input('goodsUrl');
        echo $js->submit("ランキングに入れる",array(
            'url'=>'mkHtml',
            'complete'=>'$("#data").append(XMLHttpRequest.responseText);'
        ));
        echo $form->end();
    ?> 
※CakePHPのバージョンはcakephp-cakephp-1.3.15-9-gacd25c3.zip

[PHP]配列の要素数の取得方法

javascriptみたいに「配列名.length」じゃだめで、
「count($配列名)」とのこと。

下記で勉強。

[PHP] 配列変数の長さを取得する

2012年10月2日火曜日

subversiveで管理したくないディレクトリとファイルを除外する。


EclipseのSVNを使用するためのプラグインであるsubversiveであるが、
log出力先フォルダなどのアプリケーションが生成するリソースは
基本的にはSVN管理をしたくない。
そうでないと、logが出力されただけで、プロジェクト全体がmodifiedの
ステータスになってしまうと、紛らわしい。

そこで、特定のディレクトリやファイルをsvn管理から除外した。

【手順】

1.対象のフォルダを右クリック→「チーム」→「プロパティの設定」を選択。
  以下の画像のように設定。


2.対象のフォルダをコミット


これでlogが出力されてもmodifidにならなくなった。

少し気になるのは以下のサイトでは、一度コミットされているリソースは
一度deleteしてaddしなおさなきゃ有効にならない、って言ってるんだよな。
なんでだろ。できちゃった。

Eclipse Subversive 特定のファイルをignoreする †
svn:ignoreでtmpフォルダ以下を無視するやり方






2012年10月1日月曜日

[CakePHP]XAMPP+eclipse+CakePHPでxDebugを使用する。

このページを見ていけました。
「デバッグのお気に入りの編成を選択して今作ったデバッグの構成を追加」
っていう一文が非常に大事でした。

CakePHP1.3/ 開発環境構築(Windows編)


※それぞれのバージョン
   CakePHP:cakephp-cakephp-1.3.15-9-gacd25c3.zip
  eclipse:pleiades-e3.5-php-jre_20100226 
   PHP:Version 5.3.1

2012年9月26日水曜日

[CakePHP]$form->create()の第一引数

このドキュメントみても$form->create()の第一引数にnullを入れることで、
そのformは特定のモデルを使用しないようにすることができると書いているが、
 本当のところはfalseを入れなくてはだめみたい。
    <?php
        echo $form->create(false,array(
            'type'=>'post',
            'action'=>'mkHtml'
        ));
        echo $form->input('goodsUrl',array('id'=>'goodsUrl'));
        echo $js->submit("作成",array(
            'url'=>'mkHtml',
            'update'=>'#data'
        ));
        echo $form->end();
        echo $js->writeBuffer(array('inline'=>false));
    ?>
てviewに書くと、出力されるhtmlは以下のようになる。
<input name="data[goodsUrl]" type="text" id="goodsUrl" /> 
もし、$form->create()の第一引数にfalseを設定しなかったら以下のように
モデルが配列の最初のレベルに来る。
<input name="data[theme][goodsUrl]" type="text" id="goodsUrl" />
 現在のコントローラーのデフォルトのモデルは"theme"。

 ※CakePHPのバージョンはcakephp-cakephp-1.3.15-9-gacd25c3.zip

[SVN]無料のSVNサーバ Assembla

いつもはノートPCで開発しているけど、たまにはデスクトップの端末で
開発をしてみたい、と思い、ソースコードをサーバに置こうと考えた。

無料でsvnのレポジトリが作れてしまう、以下のサービスを使ってみた。

https://www.assembla.com

今のところ非常に使いやすい。

やり方は、現在のSVNのサーバを以下のようにコマンドラインでエクスポート

svnadmin dump 【リポジトリロケーション】 > 【バックアップファイル名】
 
 出力された【バックアップファイル名】はzip圧縮する。
 
でassemblaのプロジェクトの管理画面のImport/Exportタブから
zipファイルをアップロードする。
 
何分か待つ。
 
画面がrunninng..的な画面でとまったように見えるけど、
しばらく経つと、数字が上がりはじめる。
 
 
【気になる点】
色んなブログで無料で無期限で使える(容量制限はあるみたい)
と書いてあるけど、本当に無期限なのかな。
assemblaのサイトに説明が見つからなかった。 
 
 【注意点】
移行した後、他の端末でchckoutする場合は元の端末と同じURLとなるように
checkoutする。外部サービスのcallbackURLとして設定しているから。 

2012年9月22日土曜日

[Error記録]Twitter Oauthログイン時のエラー

今朝最初にログインしようとしたとき、こんなエラーが出た。


で、修正も何もせずにすぐに同じようにログインしようとしたら、
正常にログインできた。

なんなんだろ、request_tokenがTwitter側からうまく帰ってくる前に処理が進んじゃってるからかな。


 【2012/10/03追記】
この現象、朝PCを起動して初めてログインしようとすると発生するんだよな。
今日も発生したので、「戻る」を押して、もう一度「許可」を押したら発生した。
その際、
$json = $consumer->get($accessToken->key, $accessToken->secret, 'http://twitter.com/account/verify_credentials.json', array());
で得られる$jsonをログに出力してみたんだけど、その結果が、
 {"error":"Could not authenticate you.","request":"\/account\/verify_credentials.json?oauth_consumer_key=・・・・・

"Could not authenticate you."というメッセージがあった。

これで検索すると、Could not authenticate you
というページでtwitterのリクエスト先のURLにhttpsをつければよい、
といった意見があったので試してみよう。
またユーザ側のクライアントの設定で「常にSSLを使用する」、といった設定をしている 場合に発生するとのこと。 クライアントとはおそらくブラウザのこと。 こちらも試してみよう。

【2012/10/03追記②】

ちなみに、本日の追記の時は、一度失敗したCallbackURLをもう一度
ブラウザでたたいたので、シミュレーションだったのだが、先ほど、
お昼休みにPCを落として、またログインしてみたら発生して、
ログを見たら新たな発見があった。
2012-10-03 16:50:15 Debug: requestToken
2012-10-03 16:50:15 Debug: oauth_token=XXXXXXXXXXXXXXXXXXXXXXXXX&oauth_token_secret=XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXx
2012-10-03 16:50:15 Debug: accessToken
2012-10-03 16:50:15 Debug: 
2012-10-03 16:50:15 Debug: Notice (8): Trying to get property of non-object in [C:\xampp\htdocs\rakuzon\app\controllers\users_controller.php, line 55]
2012-10-03 16:50:16 Debug: json
2012-10-03 16:50:16 Debug: {"error":"Could not authenticate you.","request":"\/account\/verify_credentials.json?oauth_consumer_key=
requestTokeをセッションから取得することはできているのだが、
accessTokenをTwitter側から取得できていない、ということがわかった。
つまり以下がうまくいっていないようだ。
$accessToken=$consumer->getAccessToken('http://api.twitter.com/oauth/access_token',$requestToken); 
【2012/10/04追記】
上記のURLをhttpからhttpsにしてもダメだった。
なんでだろうなー。

【2012/10/05追記】
何も修正していないのに、なぜか今日エラーが出ずにいけた。

2012年9月21日金曜日

[PHP]boolean のfalseはechoしても何も表示されない。

PHPのTRUEと FALSEについて 値を返すというけれど、なぜ「1」かブランク(何もなし)なのでしょうか?


このサイトによると以下とのことです。
どおりでなんにも表示されないと思った。


-----以下抜粋---------------------------------------------------------------------------
echo false;
とやると、falseがstring型へ自動型変換という手順を踏んでechoの対象になりますが
PHPの場合は falseをstring型へ変換すると ""(空文字列)になります。
(別の言語では"false"という文字列に変換されるものが多いですね)
よって、出力されません。
----ここまで----------------------------------------------------------------------------

[CakePHP]layoutってcontrollerがrenderする前に処理してないか?

※このpostは前半部分間違った推測をしています。
 後で本当の原因がわかったので、最後に追記として書いています。


全てのControllerで共通してdefault.ctpというlayoutを使っている。
そこには、もしAuth認証でログイン済みであれば、ユーザ名と
プロフィール画像を表示するようにしている。

つまり、全ての画面で共通なロジックだからだ。

もちろんlogoutでレンダーされる画面にもそのlayoutが適用
されるんだけど、なぜかlogout画面でもユーザ名と
プロフィール画面が表示されてしまう。
赤で囲った部分はログイン状態でなければ表示されないはずの部分。



仮説だが、
「layout内の処理はControllerのrender処理の前に行われる。」
のではないか。

ちなみに、ログアウト画面表示後、TOP画面等を表示すると、
ちゃんとユーザ情報などは表示しないでくれていた。
なので、logout()の処理自体はどこかで動いていて、セッションから
ログイン情報を消してくれているはず。
そして、layout内の<?php echo $content_for_layout ?>を赤枠の部分より
上に配置してみたが、相変わらず表示された。

つまり、viewの部分がlayoutより上にあったとしても、処理としてはlayoutが先に行われて
いるということではないか。

仕方がないので、layout内でlogout後かどうかを判別する仕組みを入れた。
コントローラー側は以下の3行目にログアウト後であるフラグを入れた。
ちなみに、ここで$this->session->write()を使わず$this->set()を使ったのは、
前者だと、おそらく次以降の遷移後にもフラグが立ちっぱなしになってしまうので。
setは一度のrenderでしか有効でない。
 public function logout(){
  $this->Auth->logout();
  $this->set('logoutFlg',true);
 }


つづいてlayout側。(抜粋)
2行目の&&以降で、フラグに値が入っているかを確認している。
入っていなければ、logout後ではないのでユーザ情報を表示する。
 <?php
  if(!empty($auth['User']['id']) && empty($logoutFlg)){
   echo $this->element('loginInfo');
  }else{
   echo $this->element('loginLink');
  }
 ?>

※本当はせっかくtrueが入るからempty()とか使いたくなかったけど、
logout後以外は絶対に値がはいらないから$logoutFlgみたいにbooleanで
判別しようとすると、エラーになっちゃうんだよね。

こうすれば、ログアウト画面のユーザ情報は消えてくれた。
つまり、まとめると、

「layout内の処理はControllerのrender処理の前に行われる。
ただし、layout内でController側で設定された変数が必要な場合は、
Controllerの処理を待って処理をする。」

あくまで仮説です。
誰か、本当のところを知っている人教えて下さい。


【追記】
すみません。書いた直後に原因が発覚しました。
上で書いたことと全然違ってました。

自分はAppControllerのbeforeFilterで
    function beforeFilter(){
       $this->Auth->allow('*');
       $auth=$this->Auth->user();
       $this->set('auth',$auth);
    }
としていました。
$this->Auth->logout()はsessionにあるユーザ情報は消しますが、
set()された$authは消しません。(auth.phpのソースコード参照)
なので、logoutアクションでrenderされる画面にユーザ情報が表示されて
しまったということです。
そこで、Authのユーザ情報の取り出しはview側(layout)にて行うように
修正したところ、上記で追加したlogoutFlg無しでも正常にlogout画面が
ユーザ情報無しで表示されました。

そもそも、AppControllerにAuthのユーザ情報の取り出し処理を入れていた理由は、
全画面でやるので、全viewのファイルに書くのは嫌だなと思っていたからです。
でも、layoutに書いておけば一箇所でよいので、layoutに移しました。









[CakePHP]viewで繰り返し使う部分を外だしする(elements)

elementsという機能を使えば、viewで度々使う部分を外部ファイルにして
呼び出して使うことができる。

たとえば、どの画面にも出ている「○○さんがログイン中」とか。

以下のサイトがわかりやすい。

CakePHPでビューからエレメントを呼び出す

[CakePHP]AppControllerと普通のControllerのcomponentの設定の関係

AppControllerで設定したcomponentを普通のControllerで重ねて設定すると
うまくマージしてくれないみたい。

以下のようにapp_controller.phpで設定するとする。
var $components = array('Auth','Session');
そして普通のControllerであるusers_controller.phpに以下のように設定するとする。
※まぁ、AppControllerで設定しているものを重ねて設定することはないのだけど、
 消し忘れた場合など。
var $components = array('Auth');
すると、以下のようなエラーが発生
Notice (8): Trying to get property of non-object [APP\controllers\users_controller.php, line 51]

 たぶん、コンポーネントの設定のマージがうまくいっていないんだろうな。
以下で指摘した点と同じ原因かもしれない。

[CakePHP]AppController でコンポーネント設定するとsessionコンポーネントが使えない。

なので、users_controller.phpの方のcomponentsの設定を消したらう動いた。

※CakePHPのバージョンはcakephp-cakephp-1.3.15-9-gacd25c3.zip


※追記:
この後、もう一度、users_controllerの方のcomponentの設定を戻してやっても
動いた。原因は違うところにありそうです。すみません。

[SyntaxHighlighter]右上の"?"を非表示にする

ずっときになっていた以下の赤で囲った右上の"?"。




 消す方法がわかりました。以下のサイトの中程に書いてあります。

Bloggerにソースコードをハイライトする SyntaxHighlighterを導入する

[SyntaxHighlighter]bloggerにソースコードをきれいに貼り付ける

ソースコードを行番号付きで、きれいに表示する このサイトを見てできました。

[SyntaxHighlighter]blogger mode だと<br/>タグが表示できない件

PHPのソースコードをブログに載せようとしたんだけど、
なぜか、ソース中の<br/>タグが表示されず、空行となってしまっていた。
そこで調べていると、英語だけど以下でblogger modeが悪さをしていることが
わかった。

syntaxhighlighter in blogger interpreting <br/> literally despite html encoding

 なので、以下の行をコメントアウトしたら、ちゃんと<br/>タグが表示されるようになった。

//SyntaxHighlighter.config.bloggerMode = true;

[API]各サービスのOauthを一括でやってくれる仕組みがあるのね

いつか試してみよう。

18.Opauth ライブラリで複数のソーシャルアカウントを試す/a>

2012年9月20日木曜日

[CakePHP]TwitterのOAuth認証とAuthコンポーネントを連携させる

Twitterのログイン認証を自分のサイトで利用できるAPIを使用し、
CakePHPのAuthコンポーネントを連携させてみた。
難しくて時間がかかってしまったので概念と実装方法を説明する。

 概念


まずは概念から。
流れは以下の三つの図の通り。図1→図3という時系列で処理が進んでいく。
APIを使用する側のサイトをConsumerと呼ぶらしい。
また、パソコンの絵はユーザの端末を表している。
(excelで図を作ったが、bmpファイルにしたらぼやけて汚くなっちゃった。
どうすればきれいな図の画像ファイルできるかな。)

図1



図2




 図3



 実装方法 

 

1.Twitter API https://dev.twitter.com のサイトで自分のサイトの情報を登録。


 自分のTwitterアカウントでログインしたら右上にマウスを持っていくと現れる「My Applications」を押す。次の画面で「Create a new application」を押す。

現れる入力画面で自分のサイトの情報を入力する。
「Callback URL」以外は適当でもよさそう。
Callback URLは上の概念の「⑦Callback URLにリダイレクト」でTwitter側から
リダイレクトされる際のURLである。
このURLにはAuthコンポーネントで使用するモデルのコントローラに
専用のアクションを用意し、そのURLを指定する。
私の場合はUsersコントローラにtwtrCallback()というアクションを作ったので、

http://127.0.0.1/rakuzon/users/twtrCallback

とした。まだ、開発用なので自IPアドレスを指定している。
一つ注意点として、localhosotは指定できないので127.0.0.1を指定する。
このサイト参照。 Twitter アプリケーションのコールバックURLにlocalhostを指定する

2.Cnsumer KeyとConsumer Secretを控えておく


システムがTwitterのAPIを使うための、「システムに一つ」のIDとパスワードのようなもの。
「My applications」画面から、自分が今登録したサイト名をクリックした画面で、
Detailsタブを参照することにより確認できる。
これでTwitter側で行う作業は終わり。

3.OAuthを使うためのCakePHP用のライブラリをダウンロード、インストール


Consuming OAuth-enabled APIs with CakePHP

上記でダウンロードできる。
ただ、CakePHP 1.X系を使用している場合は、
「get the version for the older CakePHP 1.x.」
というリンクからダウンロードする。

自分はcakephp-cakephp-1.3.15-9-gacd25c3.zipを使っているが、
最初2.X用のものをダウンロードし使ってみたが、動かなかった。

ファイルを解凍したら、app/vendros直下にOAuthというフォルダをフォルダごと
配置する。これでインストールは完了。

4.ソースコードを書く


コメント増量中。
コメント中の丸の数字は上記の概念図の丸の数字に対応している。
まずはAuthで使うモデルのコントローラ。
私の場合は、users_controller.php
<?php
//インストールしたライブラリを使用するためにこの1行を入れる。
App::import('Vendor','oauth',array('file'=>'OAuth'.DS.'oauth_consumer.php'));


class UsersController extends AppController {

    var $name = 'Users';
    var $components = array('Auth');

    function beforeFilter()
    {
        $this->Auth->allow('index', 'login', 'twtrCallback');

        //Authの認証で使うIDとパスワードはデフォルトでは'username'と'password'だが
     //それぞれaccess_token_keyとaccess_token_secreteに変更する。当然Usersテーブルにも同カラムを準備している。
        $this->Auth->fields = array('username' => 'access_token_key', 'password' => 'access_token_secret');
       
        //これをやらないと、app_controllerで行った設定が反映されないらしい。
        parent::beforeFilter();
    }
    //①で呼び出されるURL
    public function index() {
        $consumer=new OAuth_Consumer(
            '【2で控えたご自分のCnsumer Key】',
            '【2で控えたご自分のCnsumer Secret】');

        // ②、③ request_tokenを取得。
        //認証後、「http://127.0.0.1/rakuzon/users/twtrCallback」にリダイレクトする
        $requestToken=$consumer->getRequestToken(
        'http://twitter.com/oauth/request_token',
        'http://127.0.0.1/rakuzon/users/twtrCallback');


        // 認証後、アクセストークンを取得する際に必要なので保存
        $this->Session->write('request_token',$requestToken);

        // ④、⑤Twitterの認証ページにリダイレクト
        $this->redirect('http://twitter.com/oauth/authorize?oauth_token='
        .$requestToken->key);
    }

    //⑦のリダイレクト先URLとなるアクション。
    public function twtrCallback() {
        if (isset($this->params['url']['denied'])) {
            echo 'access denied';
            return;
        }
        $consumer=new OAuth_Consumer(
            '【2で控えたご自分のCnsumer Key】',
            '【2で控えたご自分のCnsumer Secret】');
        $requestToken=$this->Session->read('request_token');

        //access_token取得する。
        //⑧、⑨ access_tokenはユーザごとに払い出されるもの。
        $accessToken=$consumer->getAccessToken(
            'http://api.twitter.com/oauth/access_token',$requestToken);
        //⑩、⑪ access_tokenを使用し、ユーザ情報をTwitter側から引き出す。
        $json = $consumer->get($accessToken->key, $accessToken->secret, 'http://twitter.com/account/verify_credentials.json', array());
        $twitterData = json_decode($json, true);

        //⑫モデルに渡す。モデルでは保存が行われている。
        $this->User->update(
        Array(
            "user_id" => $twitterData['id_str'],
            "user_name" => $twitterData['screen_name'],
            "access_token_key" => $accessToken->key,
            "access_token_secret" => $accessToken->secret,
            "img_url" => $twitterData['profile_image_url']
            )
        );
       
        //⑬Authのlogin()処理のために型にaccess_token_keyとaccess_token_secretを入れてあげる。
        $user['User']["access_token_key"] = $accessToken->key;
        $user['User']["access_token_secret"] = $accessToken->secret;
       
        //⑬Authのlogin処理
        $this->Auth->login($user);

        $this->redirect('/main/index');
    }

    public function logout(){
        $this->Auth->logout();
    }

    public function login(){
    }
}

users/indexがユーザがまずログインするときにアクセスするURLとなる。
つまり、ログインボタンなどに設定するURLだ。
twtrCallback()がTwitterからのリダイレクト先のURLとなる。
概念の「⑦CallbackURLにリダイレクト」。
63~70行目はログインするごとにユーザの情報をアップデートする
ことを意味している。

つづいて全てのコントローラーが継承するapp_controller.php。

class AppController extends Controller {

    function beforeFilter(){
    //全ての画面でログイン状態のチェックを行うため、
    //authのユーザ情報を取得する。
        $auth=$this->Auth->user();
        $this->set('auth',$auth);
    }
}

なお、view側で$authという変数に値が入っているかどうかで
ログインしているかどうかを確認する。全画面でログイン状態確認を
行うのでapp_controllerで行っている。
$this->session->check('Auth');で最初チェックしようと思ったのだが、
なぜかlogout後もtrueが帰ってきてしまう。
値の中身は空だったけど、変数の型が残っててもtrueとなってしまうからかも。
このあたりあまり定かではないけど、 ログイン状態確認の処理は、
後々Cookieを利用した自動ログインの機能を
追加する予定で、大きいプログラムになりそうなので、view側でなく、
controller側にあったほうがいいかな、と思い、深追いしないでおく。


そして、モデル。user.php

<?php

class User extends AppModel {

    var $name = 'User';

    public function update($user_data)
    {
        $user = $this->find('first', array('conditions' => array('user_id' => $user_data['user_id'])));
        if($user) {
            $user_data['id'] = $user['User']['id'];
        }
        $this->create();
        $this->save(Array("User" => $user_data));
    }
}

9~12行目でDB中にあるuser_idとTwitterから引っ張ってきた情報のuser_idを
引き合わせて、同じユーザ情報にはきちんと上書きしてくれるようになっている。
user_idというのはTwitterでTwitterアカウント一つ一つに割り当てられる数字のID。

このモデルに対応するテーブルは以下。
CREATE TABLE IF NOT EXISTS `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `user_id` int(64) NOT NULL,
  `user_name` varchar(64) NOT NULL,
  `access_token_key` text NOT NULL,
  `access_token_secret` text NOT NULL,
  `img_url` varchar(512) NOT NULL,
  `created` datetime NOT NULL,
  `updated` datetime NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `user_id` (`user_id`)
) ENGINE=MyISAM  DEFAULT CHARSET=utf8 AUTO_INCREMENT=6 ;


users_controller.phpの80行目で、ログイン後にmain/indexに飛ばしているが、
飛ばし先でどのようにauthを扱っているかを解説するためmain_controller.phpを載せる。

<?php
//3でインストールしたライブラリを使うための1行
App::import('Vendor','oauth',array('file'=>'OAuth'.DS.'oauth_consumer.php'));

class MainController extends AppController {

    public $name = 'Main';
    public $uses = array('Item','Tag','Theme');
   
    //Authを使うため。
    var $components = array('Auth');

    var $paginate = array(
            'Theme' => array (
            'limit' => 10,
            'order' => array(
                'theme.id' => 'asc'
            )
        ),
            'Tag' => array (
            'limit' => 10
        )
        );

    function beforeFilter(){

    //未ログイン状態でもindexにはアクセスを可能とするため。
        $this->Auth->allow('index');

        parent::beforeFilter();
    }

    function index(){
        //この辺はもともとあった処理
        $this->Theme->hasMany['Item']['limit'] = 5;
        $this->Theme->hasMany['Item']['order'] = 'Item.id asc';
        $this->Theme->unbindModel(array('belongsTo'=>array('User')),false);
        $themes = $this->paginate('Theme');
        $tags =   $this->paginate('Tag');
        $this->set('themes',$themes);
        $this->set('tags',$tags);
    }
}

11行目は全部のコントローラに設定するものなので本当はapp_controllerにもたせ
たかった。でもなぜかapp_controllerにもたせることができなかったので、各コントローラで
設定。ここ参照。[CakePHP]AppController でコンポーネント設定するとsessionコンポーネントが使えない。
28行目でindexを許容している。ログイン状態によって同じ画面でも出す部品を
変えようとしているだけだから、基本的に全部許容なんだよね。
だから、$this->Auth->allow(*);にしてアクセスさせない、登録系や管理系の画面だけ
$this->Auth->deny();で設定すればいい気もする。


つづいて、実際にTwitterのユーザ名やTwitterのプロフィール画像を表示するmain/index.ctp。
こいつは抜粋。

        <?php if(!empty($auth['User']['id'])): ?>
        <div>
            <img src = "<?php echo $auth['User']['img_url'];?>"/>
            <br/>
            <?php echo $auth['User']['user_name'];?>
        </div>
        <?php endif;?>


1行目でcontroller側で取り出したauthのユーザ情報があるか確認。
無ければ、未ログインと判断し、3行目のプロフィール画像や5行目のユーザ名は表示しない。
この部品は全画面に共通的に使おうと思う。

あとは、必要に応じてlogin.ctpやlogout.ctp等のviewも作成する。

5.課題


この実装だと、ブラウザを閉じてもう一度アクセス、ログインしようとすると、
また、Twitterのアプリケーション認証画面に飛ばされて承認ボタンを押す必要
が出てくる。
それだと面倒くさいのでCookieを使用して自動ログインを実装してもいいかもしれない。

6.参考にしたURL


CakePHP で OAuth 認証を使ったログイン認証・保持や会員データの保持・更新をするコード
[プログラミング][PHP][CakePHP][twitter]CakePHPでOAuth
CakePHPでOAuthを使ったログインと自サービスのログインの両方に対応させてみました。


※CakePHPのバージョンはcakephp-cakephp-1.3.15-9-gacd25c3.zip