The Croton
Latest Entries
Moved
- Date
- 2008-10-01 (Wed)
- Category
- diary
最後のポストからかれこれ1年ちかく空いていますが、生きています。しばらく前に新しいサイトでやり直していました。たぶん。
最近また忙しくなって、あんまり書けていませんが、もう少しペースをあげていけたらと思っています。
I don't think any English readers here, but just to let you know, I moved to other blog now. I don't think I will update this site anymore. Follow the link above, please.
I will redirect Feed to new site soon, until the end of 2008, I think:P
SPL: RecursiveIterator
- Date
- 2007-10-29 (Mon)
- Category
- php
PHP5 からは、Standard PHP Library (SPL) 関数 というのが標準でついてきます。標準 PHP ライブラリとかいったら、すぐに使える便利なライブラリなどを想像してしまう僕はおバカさんかな…標準 Interface / Design Pattern 集とでも云ってくれたほうが、僕にはわかりやすいかったかも。実際の所、ほとんどのコードは自分で実装しなければならない。ただ例えば、今回扱うような foreach の挙動をクラスの側から操作できる、というのはなかなか魅力的だし、今までの PHP にはなかった事ですね。
でも使っている人が少ないのか、コードサンプルがあまり見当たりません。今回実装した RecursiveIterator など、一度ハマりどころがわかれば全然簡単なものの、わからないときはさっぱりわからないもの。いわゆる Design Pattern 集に出てくるような内容なので、晒すのもすこし気後れしますが、まぁ何かの足しにはなるでしょう。というわけで以下コード。
コード
FooNode.php
<?php
class FooNode implements RecursiveIterator { public $name; protected $parent = null; protected $children = array(); protected $cursor = 0;
public function __construct ($n = null) { $this->name = (String)$n; }
public function __toString () { $prefix = null; if (!is_null($this->parent)) { $prefix = (String)$this->parent . '/'; } return $prefix . $this->name; }
public function setParent (FooNode $v) { $this->parent = $v; }
public function add (FooNode $v) { $this->children[ $v->name ] = $v; $v->setParent($this); return $v; }
//Iterator public function current () { if ($this->key() !== false) { return $this->children[$this->key()]; } else { return null; } }
public function key () { $keys = array_keys($this->children); $length = count($keys); if (($length === 0) || ($length <= $this->cursor)) { return false; } return $keys[$this->cursor]; }
public function next () { if (count($this->children) > 0) { $this->cursor = $this->cursor + 1; } }
public function rewind () { $this->cursor = 0; }
public function valid () { $result = false; if ($this->current() instanceof FooNode) { $result = true; } return $result; }
// RecursiveIterator public function getChildren () { return $this; }
public function hasChildren () { //trigger_error($this->name . " hasChildren?"); return !empty($this->children); }
// static function public static function traversal (FooNode $f, $d = 0) { $count = 0; foreach ($f as $node) { //$prefix = str_repeat("", $d); //echo $prefix; echo $node; echo "\n";
if ($node->hasChildren()) { $count += FooNode::traversal($node->getChildren(), ($d + 1)); } ++$count; } return $count; }
}
main.php
<?php
require_once('FooNode.php');
$a = new FooNode('root');
$b = $a->add(new FooNode('hoge')); $b->add(new FooNode(0)); $b->add(new FooNode(1)); $b->add(new FooNode(2));
$c = $a->add(new FooNode('dims')); $d = $a->add(new FooNode('dinges'));
$e = $c->add(new FooNode(3)); $e->add(new FooNode('a')); $e->add(new FooNode('b')); $e->add(new FooNode('c')); $c->add(new FooNode(4)); $c->add(new FooNode(5));
$f = $d->add(new FooNode(6)); $f->add(new FooNode('d')); $f->add(new FooNode('e')); $f->add(new FooNode('f')); $d->add(new FooNode(7)); $d->add(new FooNode(8));
$c = FooNode::traversal($a);
echo "$c times traversed.\n";
使い方
同じディレクトリに main.php と FooNode.php というファイルを用意して、コマンドラインから php main.php とタイプしてみてください。
簡単な解説
FooNode::traversal() というのが、Recursive Iteration をするメソッドです。僕は getChildren と hasChildren が返す値について少々悩みました。hasChildren は Loop の外側から見ると、子供の子供があるかどうかを確認するメソッドですが、この実装では(RecusiveIterator ではなく)Iterator がオブジェクトの子要素を iterate するようになっているため、hasChildren が呼び出されるのは既に Loop が回っている先。だから直近の子供を確認しているのですね。
ただまぁこの traversal メソッド内の if 文で hasChildren なのはちょっとダサい気がするので、外部 iterator を使った実装に変えた方がいいかもなぁ。でも実際には db が絡むからこれでのいいのかもしれない。
あと Iterator::next() や Iterator::current() が 配列関数の next() や reset() とは少し挙動が違います。僕が前に Iterator の実装をした時は同じようにしていたのですが、今回 http://www.phpguru.org/downloads/Tree/Tree-2.0.0.tgz/Tree-2.0.0/ を見つけてこのようにしました。
今よく Manual を見直すと void RecursiveDirectoryIterator::next (void) や void RecursiveDirectoryIterator::rewind(void) とあるので、僕が間違っていたんですね…
また時間を見つけて SPL についても書いてみたいと思っています。
Status Code を返す / Raw HTTP Request Body を見る
- Date
- 2007-10-22 (Mon)
- Category
- symfony
細かいネタをいくつか。
API を作る時には Status Code を正しく返す
外側に公開するときはなおさら、内側で使う時でも、API を作る時に HTTP ステータスコードを正しく使おう というのは最近の Web 開発のトレンド、なんて云うと怒られるかな。少なくとも、僕は暫く前から気を遣って居ます。(中には行儀の悪い Flash などは 404 を返すと、HTTP Response Body にアクセスできないとかブツブツ…)
ともかく。Symfony を使って、HTTP Response Header 中の Status Code にアクセスするにはどうすれば良いのでしょうか?非常に簡単で、任意のアクションクラスの中で以下のようにします。
$this->getResponse()->setStatusCode(400);
あれ?404 って何だっけ?という向きに、Status Code チートシート を用意しました。ご自由にお使いください。(尤も HTTP/1.1: Status Code Definitions から H3 要素を XPath で切り出して、Word で整形しただけの単純なものですが。)
連想配列化されていない Raw HTTP Request Body を見る
PHP で HTTP POST を受け付けると、$_POST という 定義済の変数 に連想配列として自動的に格納されます。便利ですね。ただ時として、POST や PUT あるいは その他拡張された HTTP メソッド(動詞) で、連想配列化される以前の生の文字列を見たいときもあるかと思います。これは Symfony に限らず PHP 入出力ストリーム を使って、以下のようにします。
$raw_input = null;
$stream = f o p e n("php://input", "r");
if ($stream) {
$raw_input = s t r e a m _ g e t _ c o n t e n t s($stream);
f c l o s e($stream);
}
else
{
throw new Exception("Input stream open failure.");
}
今気がついたけど、『enctype="multipart/form-data" に対しては 使用できません。』ってあるね…という事は、ファイルアップロードを独自処理、とかは出来ないってことか…そんな条件分けをする意味が正直理解しがたいが…まぁしたくないけどね。
以上細かいネタでした。あ〜 MovableType だと f o p e n とか特別な単語を書くと、エラーが出るようになってる…上記変な書き方になってるのはそのせいです。
Symfony Tips: Error, Output Escaping
- Date
- 2007-10-18 (Thu)
- Category
- symfony
今日は手短に symfony 設定ネタなどを。
dev 環境で標準出力への抑制
前にも『Symfony で Json を出力』で少し書きましたが、僕は symfony を使って XML やら JSON やら YAML やらをよく生成します。PHP を database の出力用 template にするわけですね。あとはがりがり javascript とか、flash なんかの client side アプリを書いていきます。効率の面から云うと PHP というかサーバ側でもう少し連携というか framework の中から出来る方がいいのかもしれないけれども、まだそこまでは至って居ません。
ともかく。僕みたいに symfony を使って XML やら JSON やら YAML を吐き出させる人にとって、dev 環境が(標準)出力に混ぜる PHP_NOTICE と PHP_WARNING が結構しんどいです。特に XML 文にエラーが混じると、正書式でなくなりプロセスが止まります。出力を止めるには…
/apps/APP_NAME/config/settings.yml
で
dev: .settings: # E_ALL | E_STRICT = 4095 # error_reporting: 4095
と、error_reporting をコメントアウトします。もちろん、これでも php.ini に設定したエラーログには出ます。
Output Escaping の設定
symfonyで開発日記 : symfony tips vol.1 で学びました。細かいですが直ぐに役に立つ事ばかりで、非常に興味深いプレゼンです。是非、video と配布資料をご覧になる事をお勧めします。というわけで、その中で僕もこれで知った、Output Escaping のやり方を備忘録がてら、紹介します。
設定は以下のように。
/apps/{app}/config/settings.yml
all:
.settings:
escaping_strategy: both
escaping_method: ESC_ENTITIES
こうすると、template で出力される変数が全て HTML 実体参照に変換されます。
どうしても変換したくない場合(例えば上述の XML を直接出したい、とか)は2つ方法があります。
String 型の変数の場合
要するに echo で直接出せるものですね。例えば actions.class.php で
public function executeFoo ()
{
$this->output = "& is ampersand.";
}
のようにあると、fooSuccess.php では
<p>Escaped: <?php echo $output; ?> </p>
<p>Original: <?php echo $sf_data->getRaw('output'); ?> </p>
とします。
Object から method を呼び出す場合
ちょっとわかりにくいですね。僕もあまりしないんですが。例えば actions.class.php で
public function executeFoo ()
{
$this->obj = new SomeObject();
}
のようにあって SomeObject インスタンスは echo() という method で、引数を直接出力できるとします。fooSuccess.php では
<p>Escaped: <?php $obj->echo('& is ampersand.'); ?></p>
<p>Original: <?php $obj->echo('& is ampersand.', ESC_RAW); ?></p>
という風に、引数の最後に ESC_RAW というおまじないを入れるといいそうです。
ところで、今日は Symfony が公開されて 2年目の誕生日だそうです。Bonne Anniversaire! お誕生日おめでとう!
自分で書いた SQL から populated Object を作る
- Date
- 2007-10-17 (Wed)
- Category
- symfony
Symfony の採用する Propel O/R マッパは非常に便利で強力ですが、複雑にネストした query など、効果が発揮できない場面はあります。あるいはそれから生成される SQL の効率が悪いとか。そんなことにいちいち気を遣う僕の頭がおかしいのかもしれないけれど、SQL を直に書いていた頃に出来た事が出来ないのは、Framework を使う理由の本末転倒だなとは感じていました。僕が知らないだけなのかもしれない。ここにある Criteria Object の Reference にある事以上をうまく解説してるサイトとかあったら誰か教えてください。
という訳で、生の SQL Query を Symfony 標準の Propel+Creole を使って使う方法をまとめてみたいと思います。Askeet Tutorial はやや古くなりつつありますが、First Reference として引いておきましょう。
symfony Advent Calendar: symfony advent calendar day thirteen: Tags
僕が最初によくわかってなかったのは、O/R マッパがあるのに生の SQL を使うのはなんだか負けた気がする、と云うか、O/R マッパを使えば、もう SQL を使わなくていいのかと思ってた。まともに考えて、SQL を Object に抽象化する、って相当難しそうだし、めんどくさいものだと。でもそれは偏見で、O/R マッパって出来ることは凄い単純で出来ることも限られてる。そのかわり理解しやすい、自動化しやすい、他のソフトウェアから扱いやすい、ということかしらん。
多分肝心なのは、既に SQL を知ってる人は、DB とお話しするのにオブジェクト O/R マッパを使う必要は必ずしも無くて、パファーマンスが必要なときとか、SQL が得意なことをする時は生の SQL を使うべき、だと云うこと。難しくないし、そんなにめんどくさくもない、ということ今回のエントリで解説してみたい。
というわけでやってみましょう。ここでは何か target に tag を付けて、分類したいとします。tag を正規化すると target と tag は m:n な関係になりますね。schema.yml は以下。
propel:
_attributes: {defaultIdMethod: native, noxsd: true}
target:
_attributes: { phpName: Target }
id:
type: integer
required: true
primaryKey: true
autoIncrement: true
content:
type: varchar(64)
description: "Target content"
tag:
_attributes: { phpName: Tag }
id:
type: integer
required: true
primaryKey: true
autoIncrement: true
name:
type: varchar(64)
description: "Tag name"
target_tag_joint:
_attributes: { phpName: TargetTagJoint }
pkey:
type: integer
required: true
primaryKey: true
autoIncrement: true
tag_id:
type: integer
foreignTable: tag
foreignReference: id
target_id:
type: integer
foreignTable: target
foreignReference: id
Fixture として Fortune 500 をネタにしましょう。fixtures/data.yml は最後に付けておきます。実際あんまりいい例ではなかったか…
こんな時に、例えば一つの tag から target object の集合が欲しくて、普通に Criteria を使うと…
$tag_name = 'car';
$c = new Criteria(); $c->add(self::NAME, $tag_name); $tag_obj = TagPeer::doSelectOne($c);
$c = new Criteria(); $c->add(self::TAG_ID, $tag_obj->getId()); $arr = self::doSelect($c);
$result = array(); foreach($arr as $joint) { $c = new Criteria(); $c->add(TargetPeer::ID, $joint->getTargetId()); $result[] = TargetPeer::doSelectOne($c); }
return $result;
こんな感じでしょうか。前述の fixture くらいの小さな集合だったら、全く問題ないレヴェルのクエリですが、もし、例えば、一つの tag から 100 の target が引けるとすると、ちょっとシンドイでしょうね。なにせ 100 回クエリが投げられる訳ですから。
SQL 言語を理解している人には当たり前過ぎかもしれませんが、この問題は以下のような入れ子になった INNER JOIN を使うと、1 回の SQL クエリで同じ集合を得ることが出来ます。(IN 述語を使う方がいいって?後述します)
SELECT
TAR.id, TAR.content
FROM
target AS TAR
INNER JOIN (
SELECT
TTJ.target_id
FROM
target_tag_joint AS TTJ
INNER JOIN (
SELECT
TAG.id
FROM
tag AS TAG
WHERE
TAG.name = ?
) AS T ON TTJ.tag_id = T.id
) AS RES ON TAR.id = RES.target_id ;
(Join を使った query は禁止とか、アクセスの桁があがると Join は遅いからこういう正規化はダメ、というのは別の話題、ということで。)
このクエリを実行すると、ResultSet オブジェクトになり、例えば配列として結果集合を取得できます。ただ他の部分との整合性として、特別にこの method だけは配列としてアクセス、というのもすこしおかしいし難しい。どうせなら、Criteria を使うのと同じく O/R マッピングをしたい。populate してみます。
$con = Propel::getConnection(); $stmt = $con->prepareStatement($query); $stmt->setString(1, $_tag); $rs = $stmt->executeQuery(null, ResultSet::FETCHMODE_NUM);
$result = array(); $cl = Propel::import(TargetPeer::getOMClass());
while ($rs->next()) { $obj = new $cl(); $obj->hydrate($rs, 1); $result[] = $obj; }
return $result;
これは、もうまるまま Base***Peer の populateObjects メソッドのコピーです。hydrate の第二引数の int は、Base*** の hydrate のソースを読めばわかりますが、ResultSet の index を 1 から降り直したもの…訳が分かりませんね。ソースを追ってみましょう。以下、symfony propel-build-model で生成された BaseTarget.php から引用します。
public function hydrate(ResultSet $rs, $startcol = 1)
{
try
{
$this->id = $rs->getInt($startcol + 0);
$this->content = $rs->getString($startcol + 1);
$this->resetModified();
$this->setNew(false);
return $startcol + 2;
}
catch (Exception $e)
{
throw new PropelException("Error populating Target object", $e);
}
}
propel-build-model した時に、schema.yml をパースして、ResultSet を指数配列で受け取るようにして(ResultSet::FETCHMODE_NUM)順番を降っているだけだったのですね。だからこれをうまく使えば一回のクエリで2つのオブジェクトを受け取る事も可能なわけです。
まとめ
Query さえ書ければ、populate の部分はほとんど写経でも動くはずです。大事な点は、Prepared Statement を使って、ちゃんと値を代入する事と、executeQuery(null, ResultSet::FETCHMODE_NUM) のようにして指数配列で ResultSet を受け取る事です。
Hydrate も仕組みを理解すれば便利に使えます。今回の例でいえば、Target のオブジェクトに 1:n な関係の値が入る場合(例えば各 Target 項目に複数毎写真を付けられる、とか)、一つの row に target と picture が一緒にかえるような SQL を書いて、一気に hydrate する、とか。
捕捉: IN 述語
今回僕が挙げたクエリの INNER JOIN を IN を使って書き直すと、こんな感じに。
SELECT
TAR.id, TAR.content
FROM
target AS TAR
WHERE
TAR.id
IN (
SELECT
TTJ.target_id
FROM
target_tag_joint AS TTJ
WHERE
TTJ.tag_id
IN (
SELECT
TAG.id
FROM
tag AS TAG
WHERE
TAG.name = ?
)
) ;
先に INNER JOIN を使ったのは…僕がそれに馴れているからで深い意味はないんですが、書いてみて、一緒に走らせてみると、項目が多くなれば INNER JOIN を使ったクエリの方が、メモリは食いますがほんのちょびっと速くなる感じでした。
Symfony snippets: Sub-selects using Propel というページに IN 述語を使ったサブクエリを Criteria オブジェクトを使って投げる、という例があります。どっちにしても、Criteria で今回のような 2 段 nest をやる方法はわかりませんし、主題としては Populate Object は難しくないYO! という事なのでよしとします。
続きに fixtures/data.yml を挙げておきます。
Preview 用に画像を crop & scale する
- Date
- 2007-09-05 (Wed)
- Category
- Processing
画像を受け取って、それからPreview 用の Thumbnail を生成する、というのは Web app でよくあるシチュエーションだと思います。自分用の備忘録として、今日思いついた方法をメモしておきます。
変換元 / 変換先がどんな画像であれ、crop & scale に必要な情報は
- 元画像 (source)
- X 座標
- Y 座標
- 幅
- 高さ
- 先画像 (destination)
- X 座標
- Y 座標
- 幅
- 高さ
この8つです。今回はこの8つのうち、4つの情報、変換元 / 変換先それぞれの幅と高さがわかっている状態から、出来るだけ、aspect 比を変えないようにするアルゴリズムを考えてみました。PHP で手続き風に書きますが、言語(当然ですが)は問いません。
function calcResizedBounds ($w0, $h0, $w1, $h1)
{
switch (true)
{
case ($w0 > $h0):
$r = processLandscape($w0, $h0, $w1, $h1);
break;
case ($w0 < $h0):
$r = processPortrait($w0, $h0, $w1, $h1);
break;
case ($w0 == $h0):
$r = processSquare($w0, $w1, $h1);
break;
}
return $r;
}
function processLandscape ($w0, $h0, $w1, $h1)
{
$src_y = 0;
$src_h = $h0;
$ratio = $h0 / $h1;
$src_w = floor($w1 * $ratio);
$src_x = floor(($w0 - $src_w) / 2);
return array($src_x, $src_y, $src_w, $src_h);
}
function processPortrait ($w0, $h0, $w1, $h1)
{
$src_x = 0;
$src_w = $w0;
$ratio = $w0 / $w1;
$src_h = floor($h1 * $ratio);
$src_y = floor(($h0 - $src_w) / 2);
return array($src_x, $src_y, $src_w, $src_h);
}
function processSquare ($side, $w1, $h1)
{
switch (true)
{
case ($w1 > $h1):
$src_x = 0;
$src_w = $side;
$ratio = $side / $w1;
$src_h = floor($h1 * $ratio);
$src_y = floor(($side - $src_w) / 2);
break;
case ($w1 < $h1):
$src_y = 0;
$src_h = $side;
$ratio = $side / $h1;
$src_w = floor($w1 * $ratio);
$src_x = floor(($side - $src_w) / 2);
break;
case ($w1 == $h1):
$src_x = 0;
$src_y = 0;
$src_w = $side;
$src_h = $side;
break;
}
return array($src_x, $src_y, $src_w, $src_h);
}
こんな風に使います。
$dst_x = 0; $dst_y = 0; $dst_w = $w1; $dst_h = $h1; $src_x = null; $src_y = null; $src_w = null; $src_h = null; list($src_x, $src_y, $src_w, $src_h) = calcResizedBounds($w0, $h0, $w1, $h1);
Symfony で Json を出力
- Date
- 2007-09-05 (Wed)
- Category
- symfony
Symfony にも、Prototype と連携する plugin や、Askeet のサンプルでも出てくる Symfony 同梱の helper などがあります。でも最終的には結局しっくりこないで、自分でみっちりチューニングした javascript を書いてしまうことが僕は多いです。。そんな時に、僕だけかもしれないけれど、Symfony で JSON 形式の書き出しをする事があります。Symfony 側、例えば app.yml に設定を集中して書いて interface を司る javascript でも同じ設定を使い回すため、とか。
で、今日すこしハマったので、tip として挙げておきます。どういう理由なのか、自分ではさっぱりわからなかったのだけれど…
例えば、以下のような Symfony の View があります。(抜粋)
var Setting = {};
<?php foreach ($arr as $key => $val): ?>
Setting.<?php echo $key; ?> = '<?php echo $val; ?>';
<?php endforeach; ?>
これを <script src="〜〜"></script> として読み込ませてみるんだけど、javascript runtime が解釈してくれない。。ソースを browser で見て、普通にテキストとしてコピペすると、使えるのに…全体が読み込まれないならまだわかるんだけど、一部だけってのも不可解すぎた。
諦めかけた時に、ふと、ページの content-type を text/javascript から、application/x-javascript に変えてみたら、全てのオブジェクトが認識されるようになりました。/apps/modules/MODULE/config/view.yml に以下のように書きました。
default: has_layout: off http_metas: content-type: application/x-javascript
white space の扱い、とかそういうのだと思うけれど。こういう事もありますよ、という事で。