WEBアプリケーション研究室 開発ノート TOP

WEBアプリケーション研究室 開発ノート 2009年05月

スポンサーサイト

-------- --:--

上記の広告は1ヶ月以上更新のないブログに表示されています。
新しい記事を書く事で広告が消せます。

    このエントリーをはてなブックマークに追加

生SQLやPDOStatementをハイドレイトするで単純なクエリーのハイドレイトをやりましたが、Doctrineで私の好きな機能の一つ、1対多のleftJoinを旨い具合にハイドレイトしてくれる奴を生SQLでやってみました。
ちなみにテーブル定義はこんな感じです。


User:
columns:
username: string(255)
password: string(255)
indexes:
myindex:
fields: username
type: unique
actAs: [Timestampable]

Phonenumber:
columns:
user_id: integer
phonenumber: string(255)
primary_num: boolean
relations:
User:
foreignAlias: Phonenumbers
actAs: [Timestampable]
そしてコードが

$conn = Doctrine_Manager::connection();
$stmt = $conn->execute('
SELECT
u.id AS u__id,
u.username AS u__username,
u.password AS u__password,
u.created_at AS u__created_at,
u.updated_at AS u__updated_at,
p.id AS p__id,
p.user_id AS p__user_id,
p.phonenumber AS p__phonenumber,
p.primary_num AS p__primary_num,
p.created_at AS p__created_at,
p.updated_at AS p__updated_at
FROM user u
LEFT JOIN phonenumber p ON u.id = p.user_id
');
$hyd = new Doctrine_Hydrator();
$comp['user'] = array('table'=>Doctrine::getTable('User'));
$comp['phone'] = array(
'table' => Doctrine::getTable('Phonenumber'),
'parent' => 'user',
'relation' => $comp['user']['table']->getRelation('Phonenumbers')
);
$hyd->setQueryComponents($comp);

$result = $hyd->hydrateResultSet($stmt, array('u'=>'user', 'p'=>'phone'));
var_dump($result->toArray(true));

    このエントリーをはてなブックマークに追加

生のSQLで作成したPDOStatementオブジェクトからハイドレイト(クエリーの結果をオブジェクトにマッピングすること)できないかとコードをたどってみました。


$conn = Doctrine_Manager::connection();
$stmt = $conn->execute('SELECT u.id AS u__id, u.username AS u__username, u.password AS u__password, u.created_at AS u__created_at, u.updated_at AS u__updated_at FROM user u');
$hyd = new Doctrine_Hydrator();
$comp['u']['table'] = Doctrine::getTable('User');
$hyd->setQueryComponents($comp);

$result = $hyd->hydrateResultSet($stmt, array('u'=>'u'));
var_dump($result->toArray());
ポイントはSQLのカラムに全てu__columnのように別名をつけます。この名前を元にマッピングしているようです。

hydrateResultSet()の2番目の引数はu__の頭の文字がキーで値がsetQueryComponentsした配列のtableオブジェクトの入ったキーを指定します。どの値をどのテーブルにハイドレイトするか指定してます。

    このエントリーをはてなブックマークに追加

DBはMySqlです。
例えば掲載されてから一週間以内のレコードを取得


$q = Doctrine_Query::create()
->select('u.*')
->from('User u')
->where('CURRENT_TIMESTAMP - INTERVAL 7 DAY < created_at');

$users = $q->execute();
こんな感じでいけます。ただCASE文は駄目なようです。

->where('(CASE WHEN CURRENT_TIMESTAMP - INTERVAL 7 DAY < created_at THEN 1 ELSE 0 END) = 1')

Exception report[Doctrine_Query_Exception]
File :/home/sites/doctrine/lib/Doctrine/Query.php
Line :343
Message :Unknown aggregate alias: CASE
まあ、普通最初のクエリを使うでしょうか問題ないですが、たとえば新しいレコードに今日から1週間以内に登録されたレコードかどうかをフラグとして取ってきてNEWマークをだしたりとか、よくやると思います。

SELECT
u.*,
(CASE WHEN CURRENT_TIMESTAMP - INTERVAL 7 DAY < created_at THEN 1 ELSE 0 END) AS is_new
FROM user u
というようなレコードは色々試しましたが1.1.1現在出来ないようです。まあ、ホスト言語側でどうにでもやりようはありますけど。あとSELECTにINTERVALを入れたりCASTも出来ないようです。

->addSelect('CAST(created_at AS DATE)')
->addSelect('(u.created_at - INTERVAL 7 DAY) as is_new')
両方駄目でした。

joinLeftなんかすごく便利に良く出来てるので、SQLを柔軟にするのは難しいでしょうね。このようなクエリーを使わなくてすむようにDB設計段階から考えて使った方が良さそうです。

もしDQLで書く方法あったらコメントお願いします。


    このエントリーをはてなブックマークに追加

GmGuardPlugin


sfGuardPluginはすごく便利なのですが、「認証に利用するIDが固定されてしまう」という点と、「ユーザーのテーブルが1対1のリレーションで二つに分かれてしまうのが気持ち悪い」という点が気になって、作ってみました。sfGuardPluginを使った方がいいのかまだ少し悩んでますが、とりあえずアップしてみます。
ご意見ありましたらお願いします。

※対応ORMはpropelのみです。
※2009/5/22
remember meの機能でたまに挙動がおかしい時があるのに気づきましたが、1つのプロジェクトで複数のサブドメイン&アプリで運用していて、cookieなど様々なものが絡み合っているので、まだ突き止めていません。どうにか時間を作ってがんばります。何かありましたらコメントお願いします。
一つのユーザーIDが一つのハッシュしか持てなくなっていたので、修正しました。↑恐らくこれが原因です。

■インストール

symfony plugin-install http://plugin.gomo.jp/plugins/GmGuardPlugin/GmGuardPlugin-1.3.1.tgz
あるいは
symfony plugin-upgrade http://plugin.gomo.jp/plugins/GmGuardPlugin/GmGuardPlugin-1.3.1.tgz

※1.*から1.3.0にUPするときはDBを落として新しく作り直してください。
※1.3.0から1.3.1にUPするときもDBを落として新しく作り直してください。

symfony propel-build-sql
mysql -u root dbname < data/sql/plugins.GmGuardPlugin.lib.model.schema.sql
または
DROP TABLE IF EXISTS `gm_guard_remember`;
CREATE TABLE `gm_guard_remember`
(
`target_pkey` INTEGER NOT NULL,
`hash` VARCHAR(32) NOT NULL,
`ip_address` VARCHAR(50) NOT NULL,
`created_at` DATETIME,
PRIMARY KEY (`target_pkey`,`hash`)
)Type=InnoDB;


■~/apps/sys/lib/myUser.class.phpの継承元を変更

class myUser extends GmGuardSecurityUser
{
}


■~/apps/app_name/config/settings.yml設定

all:
.settings:
enabled_modules: [default, GmGuardAuth]

.actions:
login_module: GmGuardAuth
login_action: signin

secure_module: GmGuardAuth
secure_action: secure

ちなみにenabled_modulesというのは~/apps/app_name/modulesに無いmoduleを使いたいときに、ここに設定します。逆に言えば~/apps/app_name/modulesに置いたmoduleは自動的に使えるようになります。

■~/apps/app_name/config/app.yml設定
*は必須です。

all:
gm_guard_plugin:
class: Account #*認証に使うテーブルのclass名
id_column: email #*IDに使うカラム名
password_column: password #*パスワードに使うカラム名
credential_class: Credential #クリデンシャルに使うクラス名
credential_column: credential_name #クリデンシャルに使うカラム名。
                       credential_classを指定した場合はそのクラスのテーブルのカラム名。
                       単独で指定した場合はclassのカラム名
save_to_session_columns: [email,name] #セッション(sf_user)に保存するカラム
remember_cookie_expire: 60*60*24*30 #自動ログイン機能のクッキー保存時間
password_hash_func: md5 #パスワードをハッシュかするPHP関数
routes_register: on #認証ページに自動でルートを設定するか
redirect_uri: module/action #ログイン成功時、リダイレクトするURL
(リクエストしたページがあった場合はそちらに優先的にリダイレクトします)
remember_cookie: gm_remember_me
remember_me_ipcheck: false
cookie_domain: .gomo.jp


■自動ログイン機能を使うならフィルターをONにする。

security:
class: GmGuardRememberFilter


1.3.1同じIDで複数のハッシュを持てるように修正。合わせて1.3.0のcookieの名前を保存するのをやめました。
1.3.0remember_cookieをDBに保存するようにしました。同じDBを使って複数のサブドメインで別セッションを使いたい時使えます。
1.2.1ログアウト時、戻り先のURLをクリアーするように変更
1.2.0クリデンシャルが1:nの別テーブルの場合に対応
1.1.0remember_me_ipcheckと cookie_domainを追加。
1.0.3戻り先のセッションを保存する挙動を見直しました。
1.0.2ログイン成功時にリダイレクトするURLをセットできるようにしました。
1.0.1secureとsignoutのアクション時に間違えてsigninにリダイレクトしていました。

[symfony 1.1.6]

    このエントリーをはてなブックマークに追加

例えば投稿日時順にならんだ全ユーザーの記事一覧ページと、特定のユーザーの記事一覧ページがあったとして、そのページを何らかの方法でサーバー側でキャッシュしたとします。
その時、見られたタイミングによってキャッシュが切れる時間がバラバラになると、この2ページで矛盾が起きる可能性があります。

これを同じ時間に切れるように出来ないかなあと・・・

今回はSmartyのキャッシュでやってみました。


$expire_interval = 180;

$smarty->caching = 2;

if(!$smarty->is_cached('test.tpl'))
{
//....いろいろ.
}

$smarty->cache_lifetime = $expire_interval - (time() % $expire_interval);
$smarty->display('test.tpl');
キャッシュの寿命時間をこのように設定してやります。後は同じにしたい他の所でも

$expire_interval = 180;
//.....
$smarty->cache_lifetime = $expire_interval - (time() % $expire_interval);
$smarty->display('foo.tpl');
とやれば、同じ時間に切れます。現在のUnixタイムスタンプを、キャッシュの更新間隔で割ったあまりが、キャッシュが切れる周期の時間から経過した秒数なので、そこから、算出してやります。

寿命の計算はキャッシュを生成する直前にやるといいと思います。重い処理を間に挟むと多少ずれます。


・・・・これ、まちがってないですよね?^^;

    このエントリーをはてなブックマークに追加

公式日本語マニュアルの$cachingについての項ですが、以前から何度読んでも意味が理解できませんでした。ふと原文を読んだら理解できるかもと思い立ち読んでみたら、やっぱり簡単にわかりました。
訳おかしいですよね・・・
簡単に説明すると


$smarty->caching = 1;
1の時はis_cached()でそのテンプレのキャッシュが有効かどうか調べた時、キャッシュファイルが作られた時間に$cache_lifetimeを足した時間と現在の時間を比較して判断します。この時$cache_lifetimeはis_cached()が呼ばれた時点の値を使用します。一方

$smarty->caching = 2;
2の時はキャッシュ作成時(display()またはfetch()が呼ばれた時)に$cache_lifetimeから有効期限を計算しキャッシュに書き込んでしまいます。is_cached()はその時間と現在時刻を比べます。キャッシュが作られた時点の$cache_lifetimeが重要なわけです。

1の利点は後から全てのキャッシュの有効期限を簡単に変更できるってとこなのでしょうか?よくわかりませんが、2でもキャッシュを消してしまえば変更出来ますよね。

マニュアルには2の利点が1ページ内でテンプレートをいくつかに分けて、テンプレによりキャッシュ時間を変えるのができるとあります・・・あれ?でも1でもそれぞれis_cached()を呼ぶ直前に時間を設定してやればいけるんじゃなかろうか??????

強いて言うなら1はページリクエストのたびに計算+比較が行われるのに対して、2は比較のみです。ものすごい微々たる負荷の差ですが、少しでも軽いほうがいいに決まってます。

もしかしたら、最初はtrue(1)のみで後から、少し良い2を思いついて2を追加したのかも知れません。
まあ、私は2を使います。

    このエントリーをはてなブックマークに追加

たとえばwww.example.comだったらフロントエンドアプリへ。sys.example.comだったらアプリケーションsysを実行したい時の.htaccessの書き方です。


RewriteCond %{HTTP_HOST} ^www\.
RewriteRule ^(.*)$ index.php [QSA,L]

RewriteCond %{HTTP_HOST} ^sys\.
RewriteRule ^(.*)$ sys.php [QSA,L]
ようは、どのフロントコントローラーを起動するか、ということなので、結構柔軟に設定できます。例えばwww.example.com/m/以下の接続はmobileアプリケーションにだと

RewriteCond %{HTTP_HOST} ^www\.
RewriteCond %{REQUEST_URI} ^/m/
RewriteRule ^(.*)$ mobile.php [QSA,L]
など。

[symfony 1.2.7]

    このエントリーをはてなブックマークに追加

(※この問題はgauさんのおかげで解決しています。せっかちな方は下方の2009/5/29追記を確認してください。)

こちらのサイトなどを参考にアニメーションGIFのリサイズに挑戦しましたがうまくいきません。


$images = new Imagick($source);
$images = $images->coalesceImages();

$width = round($images->getImageWidth() * $scale);
$height = round($images->getImageHeight() * $scale);
do {
$images->scaleImage($width, $height);
} while ($images->nextImage());

$images = $images->deconstructImages();
$images->writeImages($dest, true);
一コマになってしまいます。どうやら一番最後のこまになっている模様。そもそも

$images = new Imagick($source);
$images = $images->coalesceImages();
$images->writeImages($dest, true);
この時点で一コマになってます。

$images = new Imagick($source);
echo $images->getNumberImages();
echo $images->getImageIndex();
してみたところ読み込んだ時点で一番最後のコマにイテレータが移動しています。もしやcoalesceImagesが現在のコマから後ろの物を処理するのか?おっ!これか?と思って

$images = new Imagick($source);
$images->setImageIndex(0);
したら・・・暴走・・・CPU100%のまま帰って来ません。setIteratorIndexというのもあるので試してみたところ「そんなメソッドはありません・・・」のエラー。
どうしようもないのでとりあえず今回は

$source = '578.gif';
$dest = 'dest.gif';
$scale = 50;

exec('convert '.$source.' -coalesce -scale '.$scale.'% -deconstruct '.$dest);
execでコマンドをたたいて実行しようと思います。
PHP5.1.6
ImageMagick6.2.8
Imagick2.2.1
です。恐らくバージョンの問題でしょうか。そもそも最初にImagick2.2.2を入れたのですがこちらのサイトにある問題と同じ問題がおきてダウングレードしました。サーバーも自社管理ならもっと色々試せるのでしょうがこの辺でギブアップします。

何か情報ありましたらコメントお願いします。

※2009/5/29追記
gauさんにコメントを頂き解決いたしました。ありがとうございます。
やっぱりcoalesceImagesもdeconstructImagesも今いるフレームから後ろに処理をするようです。で、最初のフレームに設定するコマンドがsetFirstIteratorです。

$image = new Imagick($source);
$image->setFirstIterator();
$image = $image->coalesceImages();

$width = round($image->getImageWidth() * $scale);
$height = round($image->getImageHeight() * $scale);

do {
$image->scaleImage($width, $height);
} while ($image->nextImage());

$image->setFirstIterator();

$image = $image->deconstructImages();
$image->writeImages($dest, true);
しかし、めっぽう重いです。特にcoalesceImagesとdeconstructImagesが内部で一回ずつループして全画像に処理するので重いと思われます。gauさんのエントリーにあるやり方の方がループが一回だし断然早かった。また、gauさんの所では携帯に対応させる情報なんかもあります。そちらを参照してください。

    このエントリーをはてなブックマークに追加

公式ドキュメントをやっていて、また旨く動かないところがありました。
Doctrine_Record::mapValueですが、データベースに保存したくないが、オブジェクトに保持しておきたい値をもたせることが出来るように書いてあります。


class User extends Doctrine_Record
{
public function setTableDefinition()
{
// ...
//nameというカラムはありません
$this->mapValue('name');
}

// ...
}

$user = new User();

$user->name = 'jwage';
echo $user->name; // jwage
で、サンプルコードを実行してみたのですが

Unknown record property / related component "name" on "User"
エラーが出て実行できませんでした。色々試してみたところ

class User extends BaseUser
{
public function construct()
{
$this->mapValue('name');
}
と、constructでmapValueしたら動きました。詳しく追ってませんがタイミングの問題ではないかと思います。constructは__construct()の最後で呼ばれる親クラスでは空のメソッドです。

    このエントリーをはてなブックマークに追加

別に難しいことでもなんでもないんですけど、覚えられなくて何度もを見ながら組み立ててたのでメモしておきます。


date('Y-m-d H:i:s');

    このエントリーをはてなブックマークに追加

PHPからlinuxのコマンドやアプリケーションを実行する関数にsystemというのがあります。この関数はそのアプリケーションの終了まって返り値を受け取って(最後の行を)返します。PHPでブラウザにはレスポンスを返してしまいバックグラウンドでアプリを実行したい時は返り値を捨てて尚且つ&をつけてやります。


system('command > /dev/null &');
symfonyのタスクを実行する時はこんな感じでしょうか?

system('/path/to/symfony name:sendmail app_name argument > /dev/null &');

    このエントリーをはてなブックマークに追加

今までこのブログにソースを書き込むときpreタグを使って書いていました。


//ここにソース
$test = new Test();
echo $test->execute();
Dctrineやsymfonyのドキュメントを読んでいるとソースが非常に見やすく書いてあります。ページのHtmlソースを見たりfirebugで見てみたのですが結局どんなツールを使ってるのか良くわかりませんでした。

代わりにSyntaxHighlighterというツールを見つけました。いくつか微妙な点もあるのですが色々な言語に対応していてcssで見た目のカスタマイズもできるので試しにしばらく使ってみようかと思います。

どんなふうに見えるかというと

//ここにソース
$test = new Test();
echo $test->execute();
こんな感じです。デフォルトではコピーなどのボタンがあるツールバーが右上に出てソースが見づらかったので移動しました。

.syntaxhighlighter .toolbar
{
position: static !important;
padding: 3px 8px 20px 0 !important;
}

.syntaxhighlighter
{
width: 95% !important;
}
でデフォルトでは行番号が出るのですが、これがあるとコピーする時に不便なので消してみました。あとbloggerModeをtrueにしないとbrタグが表示されてしまいます。

SyntaxHighlighter.config.bloggerMode= true;
SyntaxHighlighter.config.clipboardSwf = 'http://blog-imgs-26.fc2.com/g/o/m/gomojp/clipboard.swf';
SyntaxHighlighter.defaults['html-script'] = true;
SyntaxHighlighter.defaults['gutter'] = false;
SyntaxHighlighter.all();
ところがcssでdisplayをnoneにしているらしく、画面上は見えないのですがコピーするとコピーされちゃいます。で微妙ですが少しだけいじって行番号を出ないようにしまいした。
1349行目

+ (this.getParam('gutter') ? '' + lineNumber + '.' : '')
行番号を出さない設定ならタグごと出さないように。

IEでコピーすると微妙に改行とインデントがおかしいです。
自分で作るのも大変そうだし、なんか良いツール無いですかね?
お勧めありましたらコメントお願いいたします。

    このエントリーをはてなブックマークに追加

propelは、特にCriteriaがどうも好きになれなかったのですがDoctrineは良さそうなので、とりあえず、単体で使ってみれば理解の助けになるかなあ、といじってみてます。

本家のチュートリアルをやっていてエラーが出て旨く動かないところがありました。


Doctrine::loadModels('models');
モデルをロードして

$user = new User();
$user->username = 'jwage';
$user->password = 'changeme';

echo $user->password; // outputs md5 hash and not changeme
実行したところ

Fatal error: Class 'BaseUser' not found in /home/sites/doctrine.gomo.jp/web/models/User.php on line 13
Baseクラスの読み込みに失敗しました。動かすには

$manager->setAttribute(Doctrine::ATTR_MODEL_LOADING, Doctrine::MODEL_LOADING_CONSERVATIVE);
ATTR_MODEL_LOADINGをMODEL_LOADING_CONSERVATIVEに設定してやると動くようになります。デフォルトではMODEL_LOADING_AGGRESSIVEになってます。

MODEL_LOADING_CONSERVATIVEはクラス名とそのパスを保持していて、そのクラスが必要な時に読み込まれるいわゆるLasy loadです。その際指定したフォルダにフォルダがあった場合はしたの階層もリスト化します。

対してMODEL_LOADING_AGGRESSIVEはDoctrine::loadModelsで指定したフォルダにあるクラスを一気にrequireします。指定したフォルダの直下のクラスしか読み込まないようです。

流し読みしないで、最初から順を追ってやってればこの問題には引っかからないのですが、このページより前にDoctrine::ATTR_MODEL_LOADINGを切り替える意味について詳細な説明がないので、ざっくり読み飛ばしてました。

    このエントリーをはてなブックマークに追加
上記広告は1ヶ月以上更新のないブログに表示されています。新しい記事を書くことで広告を消せます。