アソシエーションにあるけど今要らないデータはunbindModel

アソシエーションはあるけれどfindで別にツラツラデータが欲しくない場合、まず考えるのはrecursiveで階層指定することです。それだとうまく指定できない場合にはunbindModelを使います。CakePHP1.2.x.x_24.01.2008で確認してます。

//アソシエーションにあるけど今要らないデータはunbind
$this->unbindModel(array(‘hasMany’ => array(‘Mail’, ‘Pic’, ‘Post’)), false);

こんな感じに外したいモデルを指定します。
逆に付け加えたい場合にはbindModelを使います。具体例としては、関連キーで紐づいて引っ張られてくるデータの条件を一時的に付け加えたい場合等です。

//引っ張ってくるニックネームのstatus条件
$this->bindModel(array(‘hasMany’ => array(‘Nickname’ => array(‘conditions’ => array(‘status’ => ‘= active’)))), false);

こんな感じです。もともとのbelongtoやhasmanyに勝手にマージしてくれるので、追加したい条件だけ書けばOKです。

UserモデルがMail, Pic, Post, Profile, Nickname各モデルをhasmanyで持ってたとして、上記二つの指定をしたあとfindを使うと
$this->find(array(‘User.id’ => ‘= ‘.$id,));

  • Userモデルの中からidが$idのデータを取ってくる。
  • Mail, Pic, Post各モデルのデータは見に行かないからUserモデルのidに紐づいていても取ってこない。
  • Profileモデルはuserモデルのidに紐づいているデータをごっそりもってくる。
  • Nicknameモデルの中でuserモデルのidに紐づいているデータの中からさらにstatusがactiveなデータをとってくる。

ということが出来ます。

詳しい使い方はAPI for CakePHPやソースで確認してください。

プロフィール編集のコントローラーではUserモデルとUserモデルからそれに紐づいたProfileモデルとNicknameモデルだけ必要なのに、Userモデルに紐付けられてるPostモデルやPicモデルまでごっそり引っ張ってこられても邪魔です。この場合recursiveだけで深度をうまく設定することは出来ません。富豪的にごっそり呼び出して蓄えこんでもかまいませんが、それはあんまりだってことで自分なりにやり方を考えました。

はじめは、関連キーも変数なんだからいじっちゃえばいいのでは、ということでunset()を使うことを考えました。もちろんこれでも思い通りに動作します。findの前にunset($this->hasMany[‘Post’]);なんてやればアソシエーションがごっそり消せます。でもこれだと一時的に変更ではなく同じインスタンスの中だとその変更はずっとになってしまいます。もちろん自分でそうしたからですが。

何かいいやり方は無いかなとModelのAPI見ていると、そのまんま同じことをやっているメソッドがありました。unbindModelです。そりゃあるよね普通。一時的な変更にするかどうかも引数で設定できて一安心。それに加えてちょうど困っていた、紐づいて引っ張り出されてくるデータに条件を加えたい、という件まで逆のbindModelで解決しました。これで、必要に応じて必要なデータだけをとってくることが出来るようになりました。

上の例では外すのはモデル、付け加えるのは条件、としていますが当然それ以外もOKなはずです。でも例以外は試してません。

もう一点、本当は必要なアソシエーションだけbindModelでホワイトリスト式に動的に条件すべてをくっつけてやる方がよさそうな気もします。が、配列地獄で書くのがめんどくさいのでunbindModelでモデル名書くブラックリスト式にしてます。DBのテーブルが果てしなく増えてきたときにはホワイトリスト式を考えます。

広告

メールはビューで作れば楽

CakePHPからメールを送る続き。メール本文はビューで作っちゃえば楽ちん。

//メール本文を作る
$this->set('id', $id);
$this->set('mail', $mailAddress);
$this->set('token', $token);
ob_start();
$this->render(null, '/email/text/default', '/email/user_register');
$body = ob_get_clean();

$thisはコントローラーです。

app/views/layouts/email/text/default.ctp
にてテキストメールの共通テンプレートを設定します。署名等。
app/views/email/user_register.ctp
にてメールの本文を書きます。もちろんsetした変数が使えます。

あとは前述のJPHPGMailer でsetBody($body)してやればオッケーです。

CakephpからGmailで日本語メール送信

CakePHPからGmailで日本語メールを送信するやり方です。

:: PHP Mailer ::
http://phpmailer.codeworxtech.com/

PHPで日本語メールを送る – 応用編 (添付ファイル、HTMLメール) – EC studio 技術ブログ
http://techblog.ecstudio.jp/tech-tips/mail-japanese-advance.html

のPHPMailerとJPHPMailerを使います。 メール送信用のアカウントはそう頻繁に送信毎に変えるものでも無いので、ラップするクラスにまとめた方が楽です。

まあ別にGMailに限ったものじゃなくてSMTP全般に使えますが、自分が使うのがGmailなので こんな名前にしてます。 やってることは設定読み込んで自分の環境に合わせてるだけなのであしからず。

/vendors/phpmailer/jphpgmailer.php:

<?php
vendor('phpmailer/jphpmailer');
class JPHPGMailer extends JPHPMailer{
function __construct($account = 'gmailaccount', $encoding = 'UTF-8', $language = 'ja')
{
$this->set('in_enc', $encoding);
$this->SetLanguage($language);
$this->IsSMTP();
$this->set('SMTPAuth', true);
//Gmailの設定読み込んでセット
Configure::load($account);
$this->set('Host', Configure::read('Gmail.host'));
$this->set('Port', Configure::read('Gmail.port'));
$this->set('Username', Configure::read('Gmail.username'));
$this->set('Password', Configure::read('Gmail.password'));
}
}

app/config/gmailaccount.php

<?php
$config['Gmail']['username'] = 'example@gmail.com';
$config['Gmail']['password'] = 'password';
$config['Gmail']['host'] = 'ssl://smtp.gmail.com';
$config['Gmail']['port'] = 465;

私の場合、/vendors/phpmailer/以下にclass.phpmailer.php, class.smtp.php,
jphpmailer.phpを置いてます。
インスタンス生成がnew JPHPGMailer() になるだけで、あとの使い方はJPHPMailerと同じです。

バリデーションでメールアドレスの重複登録チェック2

更新時にも使えるように重複登録チェックをバージョンアップしました。

http://sane.justblog.jp/blog/2008/02/post-9f5d.html#comment-8397038
新規登録はこれでOKだと思いますが、更新の時は、エラーになってしまうのでは?
投稿: toshiyuki_saito | 2008/02/11 22:48

確かにその通りです。

 

//テーブル名のゲッタ
function getTableName()
{
return $this->name;
}
//重複チェックのバリデート
function checkDuplicate($data)
{
$cond = array(key($data) => '= '.current($data));
if(($id = $this->getID()) !== false){
$cond[$this->getTableName().'.id'] = '!= '.$id;
}
return ($this->findCount($cond) === 0) ? true : false;
}

これで更新OKなはず。

はじめ $cond[‘id’] = ‘!= ‘.$id; にしたら

Warning (2): sqlite_query() [function.sqlite-query]: ambiguous column name: id [CORE\cake\libs\model\datasources\dbo\dbo_sqlite.php, line 115]
Query: SELECT COUNT(*) AS "count" FROM "mails" AS "Mail" LEFT JOIN "users" AS "User" ON ("Mail"."user_id" = "User"."id") WHERE "mail" = ‘hogehoge@example.com’ AND "id" != ‘6’
Warning (512): SQL Error: 1: SQL logic error or missing database [CORE\cake\libs\model\datasources\dbo_source.php, line 439]

idってどのidなのかわかんないよ!って怒られたのでテーブル名を入れました。

バリデーションでメールアドレスの重複登録チェック

結論から言うと、メンバ変数+αで簡単に重複チェックできるCakePHP1.2系は偉いです。

メールアドレスの重複登録を防ごうとしたけれど、やり方がわかりませんでした。なんとなくbeforeSave()かなあと思って取り掛かりましたが、そこから先に進めず。どうやらparent::validates()がキーワードらしいのでいろいろググりました。


CakePHP – validate | Shin x blog
http://www.1×1.jp/blog/2006/08/cakephp_validate.html

CakePHP モデルの validation の拡張 | Sun Limited Mt.
http://www.syuhari.jp/blog/archives/139

参考にしたのはこの辺。validatesの返り値やエラーメッセージの入れ方をAPI for CakePHP とにらめっこしながら、頭をひねってモデルの中を書きました。

function validates($data = array())
{
if(!parent::validates($data)){return false;}
$cond = array('mail' => '= '.$this->data['Mail']['mail']);
if($this->findCount($cond) === 0){
return true;
}else{
$this->invalidate('mail', 'そのメールアドレスは登録されています');
return false;
}
}

期待通りに動くので、もちろんこれでもOKです。ただ、ん?これ普通にバリデーションで出来ね?と思ったので書き直してみました。

var $validate = array(
'mail' => array(
'duplicate' => array(
'rule' => 'checkDuplicate',
'message' => 'そのメールアドレスは登録されています',
)
),
);
function checkDuplicate($mail)
{
$cond = array('mail' => '= '.current($mail));
return ($this->findCount($cond) === 0) ? true : false;
}

うわーん。こっちなら調べる必要なかった上に一目瞭然。上は数時間、下は3分。同じ轍踏む人が減るように書き残しておきます。validates()の書き方覚えたのもいつか役に立つといいなあ。 ついでにもうちょっとだけ汎用性高いものも書き残しておきます。

function checkDuplicate($data)
{
$cond = array(key($data) => '= '.current($data));
return ($this->findCount($cond) === 0) ? true : false;
}

migration v3.6がSQLite(MDB2 v2.5.0a2)では期待通りに動かない

CakePHPでマイグレーション使うのに便利そうな
cakephp-migrations – Google Code
http://code.google.com/p/cakephp-migrations/

のmigration v3.6ですが、SQLiteだと期待通りに動きません。結論から言えばPEAR MDB2のバグだそうです。

期待通り動かない例は以下。

001_create_users_table.yml
UP:
create_table:
   users:
     mail: string
     pass: string
DOWN:
drop_table: users

これでmigrateすると期待通りに動きます。show create table statementで表示すると

CREATE  TABLE users (id INTEGER NOT NULL PRIMARY KEY, mail VARCHAR(255) DEFAULT NULL, pass VARCHAR(255) DEFAULT NULL, created DATETIME DEFAULT NULL NOT NULL, modified DATETIME DEFAULT NULL NOT NULL)

しかし、

002_add_cols_to_users.yml
UP:
add_field:
   users:
     is_active: boolean
DOWN:
drop_field:
   users: is_active

これでmigrateすると期待していた

CREATE  TABLE users (id INTEGER NOT NULL PRIMARY KEY, mail VARCHAR(255) DEFAULT NULL, pass VARCHAR(255) DEFAULT NULL, created DATETIME DEFAULT NULL NOT NULL, modified DATETIME DEFAULT NULL NOT NULL, is_active BOOLEAN
DEFAULT NULL)

ではなく

CREATE  TABLE users (id INTEGER NOT NULL, mail VARCHAR(255) DEFAULT NULL, pass VARCHAR(255) DEFAULT NULL, created DATETIME DEFAULT '1970-01-01 00:00:00' NOT NULL, modified DATETIME DEFAULT '1970-01-01 00:00:00' NOT NULL, is_active BOOLEAN DEFAULT NULL)

となってしまい、idのPRIMARY KEYが消えてしまいます。

困ったので例によって嘘英語でバグを報告してみました。するとJoelMossから返事があって、migration側はちゃんとデータを投げているのにMDB2側にバグがあるそうです。
Issue 1 – cakephp-migrations – Google Code
http://code.google.com/p/cakephp-migrations/issues/detail?id=1

migrateのソースを見ても私の能力不足で何をしているのかよくわかりません。いわんやMDB2をやなので、これ以上は無理です。PEARにどうバグ報告していいのかも分かりませんし。
現時点のMDB2 最新alpha v2.5.0a2 でダメなので、SQLiteでmigration使いたい人はMDB2がバグフィックスされるまでしょんぼり待ちましょう。

debug 2以上で表示されるSQLクエリの数を増やす

CakePHPのdebug 2以上で表示されるSQLクエリは200件までです。これを増やすには

/cake/libs/model/datasources/datasource.php
before:
var $_queriesLogMax = 200;
after:
var $_queriesLogMax = 600;
ぐらいに増やせばOKです。増やすとメモリ食うぜ!って警告が書いてあったので必要そうな分だけにします。

大量に登録する場合、デフォルトの200件だと本数が足りなくなります。クエリ自体は通ってるので単純に表示の問題のようです。ただ、増やそうと思ったけれど設定が見つかりません。それっぽいところを追いかけてみてようやく設定を見つけました。

はじめは/cake/の中身をいじりたくなかったので、
/cake/libs/model/datasources/datasource.php
を丸々
/app/model/datasources/datasource.php
にコピーしてきて
var $_queriesLogMax = 600;
としてみましたが、変化無しでした。appの方は読み込まないみたいです。 仕方なく上記の通り/cake/をいじりました。

それにしてもqueriesLogMaxで検索してgoogleでもyahooでも世界中で0件っていうのはどういうことなんだろ。一応期待通りに動作はしているものの触っちゃいけないところなのかもしれません。世界中でゼロってことは無いでしょさすがに。セッタを見逃してる予感。

_queriesLogMaxで検索したらちょこちょこ出て来ました。よかったよかった。

SQLiteでTransaction behaviorを使う

Transaction behavior | The Bakery, Everything CakePHP : Articles http://bakery.cakephp.org/articles/view/transaction-behavior

を便利に使っていたのですがSQLiteだとこんな↓エラーが出ました

Warning (2): sqlite_query() [function.sqlite-query]: near "SET": syntax error [CORE\cake\libs\model\datasources\dbo\dbo_sqlite.php, line 115]

Query: SET autocommit=0

Warning (512): SQL Error: 1: SQL logic error or missing database [CORE\cake\libs\model\datasources\dbo_source.php, line 439]

transaction behavior がおかしいのれす。あらためてtransaction behavior をザーッと見てみたところ、これMySQLで決めうちなのね…

とりあえずautocommit関連設定を全部falseにする。 さらにメンバメソッドのbeginの中を

before:
$model->query(‘SET autocommit=0’);
after:
if(strpos(ConnectionManager::getDataSource($model->useDbConfig)->description, ‘SQLite’) === false){$model->query(‘SET autocommit=0’);}

に変更。SQLiteはBEGINされると勝手にautocommitがOFFになるっぽいのでこれでよさそう。 自分のつないでいるDBがどの種類かってなんかもっと分かりやすいのがありそうな気がするけど見つけられず。暫定的にこれで。

beginとcallbackとcommitだけでいいからCakePHP用各DBのbehavior誰か書いてください。誰も書かなかったらそのうち調べて書こう。

CakePHPでmyappを作り始めるまでの準備その2(bakeでざっくり)

實松アウトプット: CakePHPでmyappを作り始めるまでの準備その1(TortoiseSVNとbake)
http://sane.justblog.jp/blog/2008/02/cakephpmyapp1to.html

の続き行きます。

  1. myapp/configの中のbootstrap.php.default, core.php.default, routes.php.default, database.php.default をコピーしてファイル名から.defaultを削ります。.default無しの方はSVNコミットの際に無視リストに入れて追加しないようにします。
  2. (SQLiteを使うなら)myapp/myapp.sqliteをつくり、これもSVNコミットの際に無視リストに入れます。
  3. myapp/config/core.phpのSecurity.saltの値を書き換えます。
  4. myapp/config/database.phpを編集します。’encoding’ => ‘utf8’を書きます。これはdatabase.php.defaultにも書き加えておきます。また、’database’ =>のところがwebrootから見たパスになるのでSQLiteを使う場合は注意です。フルパスで書いておけば気にしなくていいです。ちなみにこの4はbakeでも出来ますがリネームしたり何なりのついでに編集してしまった方が早いです。
  5. DBにテーブルを作ります。MySQLならphpMySQLAdmin、SQLiteならTkSQLiteが便利です。
  6. c:/path/to/cakephpからコマンドプロンプトを起動します。bakeを使ってテーブルの数だけモデルを作ります。
    c:/path/to/cakephp>cake/console/cake bake -app myapp
    アソシエーションだけ対話式でさっさと作ってしまいます。
  7. bakeを使ってモデルの数だけコントローラを作ります。scaffoldはとりあえず無しで。admin用のindex, edit, add, deleteも作ります。
  8. コントローラに合わせてビューもbakeでざっくり自動生成します。
  9. 作ったコントローラからadmin関連以外のメンバメソッドは消してしまいます。 同様にビューからもadminでないファイルを消してしまいます。これでadmin経由で全部のデータがwebから編集できます。
    このやりかたの利点はモデルのバリデートが活きることです。phpMySQLAdmin等でアクセスした場合には当然全部編集出来るので、そこは使い分けです。

以上です。あとは好きなだけ作りたいように作ればオッケーです。ただしこのやりかただとカラムを変更した場合、admin用のビューをまた作り直す必要があります。まだ試していませんが、admin関連の時だけscaffoldをonにした方が楽かもしれません。

CakePHPでmyappを作り始めるまでの準備その1(TortoiseSVNとbake)

それぞれやり方があると思いますが、自分なりのやり方を備忘録がてら書いてみます。書いている時点でのCakePHPのバージョンは1.2.x.xのrev6418です。WindowsXPSP1です。TortoiseSVNをつかっています。

  1. これからmyappを作ります。
  2. c:/path/to/repositoryにmyappディレクトリを作ります。そしてc:/path/to/repository/myappにTortoiseSVNでリポジトリを作成します。(私の場合アプリごとにリポジトリを分けています)
  3. c:/path/to/cakephpからコマンドプロンプトを起動します。bakeを使います。

    c:/path/to/cakephp>cake\console\cake bake -app myapp

    App : myapp
    Path: c:/path/to/cakephp/myapp
    —————————————————————
    Bake Project Skel Directory: c:/path/to/cakephp/cake/console/libs/templates/skel
    Will be copied to: c:/path/to/cakephp/myapp

    確認にyesを入力するとmyappにファイルがコピーされました。

  4. myapp/configの中のbootstrap.php, core.php, routes.phpをそれぞれbootstrap.php.default, core.php.default, routes.php.default にリネームします。
  5. myappをfile:///c:/path/to/repository/myapp/trunkにTortoiseSVNでインポートします。
  6. myappの中身をいったんまっさらに消して、SVNチェックアウトします。リポジトリはfile:///c:/path/to/repository/myapp/trunkです。この作業は馬鹿っぽいのでおそらくいったん消さずに済むちゃんとしたやり方があるはずです。

ひとまずここまで。 次はDBにテーブルを作って、bakeで一気にアソシエーション等を済ませます。