CakePHP Archive
CakePHP テーブルのカラムサイズでvalidate
モデルで文字列の長さをvalidateする際にテーブルのカラムサイズを使う方法です。
CakePHPではモデルがテーブルのカラム情報を保持しているのですが、この情報にはカラム名やデータ型の他にカラムサイズ(varchar(N)等)も含まれています。
そこでモデルのvalidateでこの値を使えば最低限テーブルのカラムに収まるかどうかはチェックできます。
ここでは以下のようなテーブルでnameとemailの文字列長をチェックします。(DBはPostgreSQL)
create table users ( id serial primary key ,name varchar(50) not null ,email varchar(100) not null );
1. AppModelにカラムサイズ取得、文字列長チェックメソッドを追加する
AppModelにカラムサイズ取得用メソッド(getColumnLength)と文字列長チェックメソッド(validatesLength)を記述します。
<?php
class AppModel extends Model {
/**
* Returns the column type of a column length in the model
*
* @param string $column The name of the model column
* @return integer
* @access public
*/
function getColumnLength($column) {
$columns = $this->loadInfo();
$columns = $columns->value;
$cols = array();
foreach($columns as $col) {
if ($col['name'] == $column) {
return $col['length'];
}
}
return null;
}
/**
* valid value length
*
* @param array $data
* @param string $fields
* @access public
*/
function validatesLength($data, $fields) {
foreach ($fields as $v) {
$len = $this->getColumnLength($v);
if (!empty($data[$v]) && mb_strlen($data[$v]) > $len) {
$this->invalidate($v . '_len_' . $len);
}
}
}
}
?>
2. モデルで文字列長をチェックする
モデルで文字列長チェックを呼びます。単に$dataとvalidatesLengthメソッドにチェックするカラムを配列で渡すだけでokです。
validatesLength内部ではテーブルのカラムサイズで文字列長チェックを行い、エラーならinvalidateメソッドを呼びます。このinvalidateメソッドに与える値[カラム名 + ‘_len_’ + カラムサイズ]がポイントです。これをモデルの$validationErrorsに持たせておくことにより、ビューで正規の文字列長を含んだエラーメッセージを動的に出力することができます。
<?php
function validates($data = array()) {
if (!parent::validates($data)) {
return false;
}
if (empty($data)) {
$data = $this->data;
}
$data = $data['User'];
// 文字列長チェック
$list = array('name', 'email');
$this->validatesLength($data, $list);
}
?>
3. HtmlExtヘルパでエラーメッセージを出力
文字列長エラーメッセージを動的に出力するメソッド(tagLengthErrorMsg)をHtmlヘルパーを継承したHtmlExtに定義しておきます。
<?php
class HtmlExtHelper extends HtmlHelper {
/**
* lengthエラーメッセージ表示
*
* @param string $field
* @param string $name
* @return string
*/
function tagLengthErrorMsg($field, $name) {
if (empty($this->validationErrors)) {
return null;
}
$this->setFormTag($field);
foreach ($this->validationErrors[$this->model] as $k => $v) {
if (preg_match("/^" . $this->field . "_len_([0-9]+)$/", $k, $m)) {
$message = sprintf("%sは%d文字以内で入力してください。", $name, $m[1]);
return $this->tagErrorMsg($field . "_len_" . $m[1], $message);
}
}
return null;
}
}
?>
4. ビューテンプレートでヘルパ呼び出し
最後にビューテンプレートで先程のtagLengthErrorMsgメソッドを呼び出します。これは引数としてカラム名とエラーメッセージ用項目名を与えます。
<!-- エラー時は「氏名は50文字以内で入力して下さい。」を表示 -->
<?php echo $htmlExt->tagLengthErrorMsg('User/name', '氏名'); ?>
<!-- エラー時は「メールアドレスは100文字以内で入力して下さい。」を表示 -->
<?php echo $htmlExt->tagLengthErrorMsg('User/email', 'メールアドレス'); ?>
ソース内に文字列長を書かなくて良い
この方法では文字列長をテーブル定義から取得するので、ソースに文字列長を書く必要がありません。もしテーブルのカラムサイズが変わってもソースはそのままで新しいカラムサイズに対応することができます。
(ちなみにtext型などカラムサイズが無いものについてはテーブル定義情報(モデルの$_tableInfo)に直接文字列長を入れるという方法もあります。)
ここでは1.1系で記述していますが、1.2系ではvalidateが柔軟な形になっているので2.のモデルでのチェックはもうちょっとスマートに書けそうです。
- コメント (Close): 0
- Trackbacks: 0
CakePHP ライフサイクルチャート
via: cakebaker » What happens if you do a request?
CakePHPのライフサイクルがチャートになっています。
URLへのアクセスから出力まで(Controller#afterFilter()まで)書かれています。こうして図になっていると理解しやすくて良いですね。ソースを読む助けにもなります。
# ちょうどこのあたりを文章化していたので実はものすごく有り難かったり;-)
- コメント (Close): 0
- Trackbacks: 0
CakePHP 任意のSQLを書く
CakePHPには強力なO/Rマッパーがあるので、普段はSQLを書かなくても良いのですが、DBの関数を呼ぶ時などやはりSQL文を直接書きたい場面が出てきます。
そこでSQL文を直接発行する方法です。
SQL文を直接発行するにはモデルのqueryメソッドを呼ぶ方法とdboのqueryメソッドを呼ぶ方法があります。モデルのqueryメソッドは結局内部でdboのqueryメソッドを呼んでいますので、どちらも取れる値は同じです。
<?php
class HogeControlle extends AppController {
$name = 'Hoge';
function index() {
// PostgreSQL
// モデル
var_dump($this->Hoge->query('select version();'));
// dbo
$db =& ConnectionManager::getDataSource($this->Hoge->useDbConfig);
var_dump($db->query('select version();'));
}
}
?>
出力値です。(同じ出力なので一つだけ)
array(1) {
[0]=>
array(1) {
[0]=>
array(1) {
["version"]=>
string(88) "PostgreSQL 8.2.3 on i686-pc-mingw32, compiled by GCC gcc.exe (GCC) 3.4.4 (mingw special)"
}
}
}
これが良いなあというのはSQLを発行した結果の値がメソッドの戻り値で取得できることです。
発行するだけなら何使っても簡単なんですが、結果を取得するのにfetchしたりするのがちょっと面倒だなと感じていました。(いやコード書いてもしれてるんですが、何となく;-))
[2007/06/07追記]
というかモデルに無いメソッドは__callでもれなくSQL文として発行されるみたいです。なので↓でいけます。(これはちょっとやり過ぎな感も)
<?php
class HogeControlle extends AppController {
$name = 'Hoge';
function index() {
$sql = "select version()";
var_dump($this->Hoge->$sql());
}
}
?>
- コメント (Close): 0
- Trackbacks: 1
CakePHP Controller#Object()が外部から呼べる
以前あったControllerのメソッドが外部から呼べてしまう問題ですが、フレームワークが修正されて問題は解決したと思っていました。
しかし、よくよく見てみるとObject()だけはまだ呼べる状態になっています。
下のようなコントローラに[http://example.com/hoge/Object]でアクセスすると”**construct”が2回表示されます。
[app/controller/hoge_controller.php]
<?php
class HogeController extends AppController {
var $uses = array();
function __construct() {
parent::__construct();
var_dump("**construct");
}
}
?>
コンストラクタを継承してHogeControllerでややこしい事をしなければ、まあ2回呼ばれても良いかもしれませんが、やはり外から呼べる状態は気持ち悪いです。
こういうのを考えるとURLからコントローラとアクションが自動的にマッピングされるより、routes.phpできっちりマッピングしておいた方が安全かなあと思ったりします。(まあ利便性とのバランスですが)
いちおうチケットは出しておいたので修正されるのを待ちたいと思います。
- コメント (Close): 0
- Trackbacks: 0
Twitter検索がRSS対応になりました
既にお気付きかと思いますが、Twitter検索がRSS対応になりました。
キーワード検索の検索結果をお好きなRSSリーダーで見ることができます。
同じキーワードを定期的にチェックしたい方は活用してみてください。
↓のような使い方が面白いかも。
-
自分のユーザ名で検索。
=>自分宛に書いた投稿をチェック -
気になるキーワードで検索。
=>キーワードに合致する投稿をチェック -
でも検索結果が0。
=>いつか誰かが書くかもしれないので、いちおうRSS購読(by @jazzanovaさん)
- コメント (Close): 8
- Trackbacks: 0
CakePHP モデルのvalidates()に注意
Model#validates()ですが、「ちょっとどうなの?」な仕様になってます。
このメソッドはModel#save()内で自動的に呼ばれるのか、コントローラのアクションメソッド内で呼ばれるのが一般的な使い方だと思います。
問題が起こるのは後者の方で、空の配列をvalidates()に渡すと、Userl#$validateで何を定義していてもtrueが返ってきます。
下のソースならUserモデルの$validateに入力チェックをいれておけば「ng」が表示されそうですが、実はUser#$validateに関わらず「ok」が表示されてしまいます。
<?php
class FooController extends AppController
{
var $uses = array('User');
function blank()
{
$this->data = array();
if ($this->User->validates($this->data)) {
var_dump('ok');
} else {
var_dump('ng');
}
exit;
}
}
?>
原因はModel#invalidFields()の以下の箇所にあります。(invalidFields()はvalidates()から呼ばれます)
[cake/libs/model/model_php4.php]
<?php
foreach($this->validate as $field_name => $validator) {
if (isset($data[$field_name]) && !preg_match($validator, $data[$field_name])) {
$this->invalidate($field_name);
}
}
?>
$dataはvalidates()に渡される連想配列なのですが、$dataに$validateのキーが存在しない場合はpreg_match自体が処理されず、invalidate()にはなりません。
なのでコントローラからvalidates()を呼ぶ時は、$dataにModel#$validateのキーに存在するかを確認する必要があります。
ちなみにModel#save()ではvalidates()の後の処理でfalseが返るようになっています。ただこれも$dataが空の配列の場合の話で、Model#$validateにキーは無い(入力チェックはしない)が、テーブルにカラムがあるキーのみの配列なら通ってしまいます。
例えば下のソースなら「ok」が表示されます。
<?php
function blank()
{
// User#$validate に id が無い場合
$this->data = array('id' => '');
if ($this->User->save($this->data)) {
var_dump('ok');
} else {
var_dump('ng');
}
exit;
}
}
?>
やはりこちらも入力チェックするキーが$dataに存在するかを確認するしか無いようです。
- コメント (Close): 6
- Trackbacks: 2
CakePHP $_GET/$_POSTの値はどこに?
PHPのスーパーグローバルの値をどのように参照すれば良いかまとめてみました。
Controller#dataやアクションメソッド引数のようにフレームワークで想定された使い方をしている分には特に問題無いのですが、ちょっと他のことをやろうとすると、どこに値が格納されているか分からず困った事がありました。
# もちろん$_GET/$_POSTを使えば値は取れますが、せっかくのフレームワークなのでなるべくその中で値を使いたいものです。
1. $_GET
$_GETの値はController#params[‘url’]に格納されます。
ちなみに$_GET[‘url’]はURLルーティング(リクエストURIからコントローラ・モデル等を決定)で、$_GET[‘file’]は[app/webroot/js/vendors.php]で参照されています。
[http://example.com/foo/index/?id=1&code=abcd&offset=10]でアクセス
<?php
class FooController extends AppController {
function index() {
// $this->params['url']['id'] => 1
// $this->params['url']['code'] => 'abcd'
// $this->params['url']['offet'] => 10
}
}
?>
2. $_POST
$_POSTの値はController#params[‘form’]に格納されます。
なお中でも$_POST[‘data’]は特別で、Controller#data/Controller#params[‘data’]にも格納されます。
[id=1&name=abcd&comment=Hello!!&data[Foo][name]=Bar]をPOST
<?php
class FooController extends AppController {
function index() {
// $this->params['form']['id'] => 1
// $this->params['form']['name'] => 'abcd'
// $this->params['form']['comment'] => 'Hello!!'
// 以下3つは同じ
// $this->params['form']['data']['Foo']['name'] => 'Bar'
// $this->params['data']['Foo']['name'] => 'Bar'
// $this->data['Foo']['name'] => 'Bar'
}
}
?>
3. $_COOKIE
$_COOKIEの値を参照する方法は用意されていません。もし$_COOKIEの値を使用する場合は直接参照するしかないようです。
ちなみにフレームワーク側はCakeSessionクラスがこの値を参照しています。
4. $_FILES
$_FILESはおおよそ$_POSTと同様にキーによって格納される場所が異なります。
まず$_FILES[‘data’]以外の箇所についてですが、これは$_POSTと同じようにController#params[‘form’]に格納されます。
つぎに$_FILES[‘data’]ですが、こちらは$_POST[‘data’]と同じようにController#data/Controller#params[‘data’]に格納されます。ここでは$_FILES[‘data’]がそのまま格納されるわけではなく、下のソースのように[$model][$field][$key]の連想配列に値を格納されます。
[cake/dispatcher.php:340]
if (isset($_FILES['data'])) {
foreach ($_FILES['data'] as $key => $data) {
foreach ($data as $model => $fields) {
foreach ($fields as $field => $value) {
$params['data'][$model][$field][$key] = $value;
}
}
}
}
$_POST[‘data’]の場合と異なり、$_FILES[‘data’]の値はController#params[‘form’]には格納されません。
なお、$_FILESの格納処理は$_POSTの値が格納された後に行われます。つまり$_POSTと$_FILESが同じキーを持つ値は$_FILESの値で上書きされてしまいますのでご注意を。
5. $_ENV/$_SERVER
$_ENV/$_SERVERの値は[cake/basics.php]にあるenv()関数で参照できます。
どちらの変数にも同じキーで値がある場合は、$_SERVERの値が優先され、次に$_ENVの値になります。双方に該当するキーが無い場合はgetenv()の値が返ります。(全てのキーが無ければnullが返ります。)
<?php
class FooController extends AppController {
function index() {
$_SERVER['foo'] = 'server';
$_ENV['foo'] = 'env';
putenv('foo=getenv');
// server を出力
var_dump(env('foo'));
unset($_SERVER['foo']);
// env を出力
var_dump(env('foo'));
unset($_ENV['foo']);
// foo を出力
var_dump(env('foo'));
}
}
?>
6. $_REQUEST/$_GLOBALS
$_REQUEST/$_REQUESTの値は特に参照する方法は用意されていません。フレームワークでも参照している箇所は見当たりませんでした。
- コメント (Close): 0
- Trackbacks: 0
Twitterで暇つぶし
またまたTwitterネタ。
Twitter 検索が某ネタのおかげで好評でした(@ieiriさん、ありがとうございました)ので、調子に乗ってこんなのも作ってみました。
携帯でTwitterのステータス(投稿)を見るだけのサイトです。
外出している時に携帯でTwitterを見ていると意外と更新されずに何度もリロードする事ありません?
Friendsをバンバン増やせば良いのでしょうけどあまり増やしすぎると今度PCで見るときがつらかったり。
知らない人のステータスをただ見るだけなのですが意外と面白かったりします。
登録等は一切不要なので「Twitterって聞くけど、何?」な方も一度どうぞ。
- コメント (Close): 0
- Trackbacks: 0
Twitter 検索
先日はじめたTwitterですが、なかなか楽しいです。
Twitterの仕組み自体ももちろん面白い良いのですが、日々ユーザが増えていき、システムが改良され(今日は日本語入力が改善されたもよう)、どんどん完成度が高まっていくさまが何だか楽しいです。
で、ちょうどCakePHPで何か作ろうと思っていたので、こんなの作ってみました。
Twitterの検索では、既にtwittersearchがあるのですが、それとは違いこちらは日本語ユーザのみの検索ができます。
興味のあるキーワードを入れて、Friends探しに使って貰えたら良いかと。
# 超見切り発車気味な公開なので、突然消える可能性もありますが。;-)
- コメント (Close): 1
- Trackbacks: 1
CakePHP SQLをログに記録
CakePHPで発行したSQLをログに記録する方法です。
DEBUG>=2にすれば画面下に表示されるSQL文ですが、DB処理後にリダイレクト等で遷移すると消えてしまいます。さらに本番稼働時は画面に表示するわけにはいかないのでやはりログに出力したいところです。
フレームワークに手を入れるのが一番簡単なのですが、バージョンが上がると面倒なので既存のDboSourceを継承したクラスにログ記録を追加します。
1. DboSourceを継承
dboはフレームワークで用意されているものだけでなく、app/model/dboにあるものもフレームワークで利用する事ができます。
ここではPostgreSQLを使うとしてDboPostgresを継承したDboPostgresLogを作ります。
ログ出力をON/OFFする定数LOG_SQLは後でcore.phpで定義します。
[app/model/dbo/dbo_postgres_log.php]
<?php
uses ('model' . DS . 'dbo' . DS . 'dbo_postgres');
class DboPostgresLog extends DboPostgres {
/**
* @var integer
*/
var $queryNo = 1;
function execute($sql) {
$ret = parent::execute($sql);
if (defined('LOG_SQL') && LOG_SQL) {
$this->log(sprintf("%d. %s", $this->queryNo, $sql), LOG_DEBUG);
$this->queryNo++;
}
return $ret;
}
}
?>
2. 作成したDboPostgresLogを使用する
DATABASE_CONFIGのDB設定でdriverを’postgres_log’を指定します。
[app/config/database.php]
class DATABASE_CONFIG
{
var $default = array('driver' => 'postgres_log',
'connect' => 'pg_connect',
'host' => 'localhost',
'login' => 'user',
'password' => 'pass',
'database' => 'foo',
'prefix' => '');
}
3. core.phpでLOG_SQLを定義
SQLをログに出力するか否かを設定するLOG_SQLをcore.phpで定義します。
LOG_SQLがtrueならSQL出力し、LOG_SQLが未定義もしくはfalseの場合は出力しません。
[app/config/core.php]
<?php
/**
* If set to true, logging sql queries
*/
define('LOG_SQL', true);
?>
これで実行されたSQL文が[app/tmp/logs/debug.log]に出力されます。
この方法はDEBUGの値に関係無くログ出力ができるのは良いのですが、DboSourcesを継承するのがイマイチです。フレームワーク(DboSources自身)で対応して貰えれば一番良いですけどね。;-)
- コメント (Close): 4
- Trackbacks: 0
- 検索
- フィード
- メタ情報
