PHPのhtmltemplate.incでオブジェクトやらフィルターやら

ひょんなことからhtmltemplate.incというHTMLテンプレートエンジンを触ることになった。

どんな感じのテンプレートなのかは

あたりで詳しく書かれている。

Smartyに慣れている人間からすると非常に使いにくい。オブジェクトが渡せなかったり、テンプレート内でHTMLエスケープ等のフィルター処理ができなかったりする。

で機能追加をしたいわけなのですが、これがなかなかhtmltemplate.incというやつは短いコードながら設計が優れていてとても機能拡張がしやすい。

というわけでやっちゃいました。

<?php
require_once 'htmltemplate.inc';

class FilterTag extends DataTag {
    function getIndex($m,$multilabels){
        $code = preg_split ('/\|/',$m);
        $val = array_shift($code);
        if ( preg_match('/^([\w\/]+)(.*)$/',$val,$matches) ) {
            $ind = parent::getIndex($matches[1],$multilabels);
            if ( isset($matches[2]) ) $ind .= $matches[2];
            $ind = "\$val$ind";
        }
        else {
            $ind = "\$val".parent::getIndex($val,$multilabels);
        }
        if ( count($code) ) {
            foreach ($code as $filter) {
                $args    = preg_split ('/\:/',$filter);
                $filname = array_shift($args);
                if ( count($args) ) {
                    $arg = join(',',$args);
                    $ind = "htmltemplate_filter_$filname($ind,$arg)";
                }
                else {
                    $ind = "htmltemplate_filter_$filname($ind)";
                }
            }
        }
        return $ind;
    }
}

class tag_fil extends FilterTag {
    var $matchregexp='/\{fil ([^\}]+)\}/i';
    var $fromstring="{fil %s}";
    var $tostring="<?php @print %1\$s; ?>\n";
}

/**
 * HTMLエスケープして返すフィルター
 * {fil foo|html}
 */
function htmltemplate_filter_html ($str) {
    return htmlspecialchars($str);
}

/**
 * 改行コードを<br />に変換して返すフィルター
 * {fil foo|nl2br}
 */
function htmltemplate_filter_nl2br ($str) {
    return nl2br($str);
}

// シングルトンクラスなのでどこから呼び出してもOK
$html =& htmltemplate::getInstance();

// 機能追加!
$html->parser->add(new tag_fil);

これで機能追加完了。filというタグを追加しました。

<?php

$val = array('foo' => '<hr>');
print htmltemplate::buffer("A{fil foo|html}B",$val); // A&lt;hr&gt;B

となります。

filタグの機能は下記のような感じ

 {fil foo}            // rvalと同じ
 {fil foo/0}          // 配列呼び出し
 {fil foo[0]}         // 配列呼び出し
 {fil foo()}          // 関数呼び出し
 {fil foo->bar()}     // オブジェクト呼び出し
 {fil foo|html}       // HTMLエスケープフィルターを呼ぶ
 {fil foo|html|nl2br} // 複数のフィルターを重ね掛けすることも可能
 {fil foo|bar:100}    // フィルターに引数を渡すことも可能

実際にそれぞれの動きを見てみよう。

<?php

function htmltemplate_filter_plus ($str,$num) {
    return $str+$num;
}
class Foo { function bar () { return 'bar'; } }

$val = array(
    'foo1' => '<hr>',
    'foo2' => array('aaa'),
    'foo3' => array('bbb'),
    'foo4' => 'rand',
    'foo5' => new Foo,
    'foo6' => '<hr>',
    'foo7' => "<hr>\n",
    'foo8' => 50,
);

print htmltemplate::buffer("
 {fil foo1}
 {fil foo2/0}
 {fil foo3[0]}
 {fil foo4()}
 {fil foo5->bar()}
 {fil foo6|html}
 {fil foo7|html|nl2br}
 {fil foo8|plus:100}
",$val);
$ php test.php
 <hr>
 aaa
 bbb
 21871
 bar
 &lt;hr&gt;
 &lt;hr&gt;<br />

 150

うまく動いてます。

なんせ古いクラスなので需要があるのかさっぱりわからないけど何かの参考になれば幸いです。

ArrayObjectは便利だけと扱いづらいね

配列のように振舞えるArrayObjectなんだけど、あくまでもオブジェクトなので結局のところ配列を期待する標準関数との相性が悪すぎて微妙。

is_arrayは偽になっちゃうし、array_shiftはうまく動かないし。

array_flipとかarray_uniqueとかには一応渡せるけど戻り値は配列になってるのでこれまた微妙。

折角配列っぽく扱えるのに標準関数に渡せないので、非常に使いどころが難しくなる上に後々の混乱の元となりそうだ。

文字列評価された時の振る舞いを定義できる__toStringみたいな感じで、配列評価された時の振る舞いを定義できる__toArrayみたいな構文を用意してくれればちょっとはマシになりそうなんだけどねー。

とにかくせめてis_arrayだけでも真になって欲しいなぁ・・・。

仕方ないので今はこんな関数用意してやってます。

<?php

function is_array_access (&$array) {
    return (is_array($array) || $array instanceof ArrayAccess);
}

List::MoreUtilsのuniq関数の実装が変わってた件

大分昔に書いたHashを使って配列をユニークにしようという記事のブコメにてList::MoreUtilsのuniq関数の実装が変わったというコメントを頂きました。

List::MoreUtils 0.25_02 では少々変更されていた。 http://cpansearch.perl.org/src/VPARSEVAL/List-MoreUtils-0.25_02/lib/List/MoreUtils.pm

はてなブックマーク - kitsのブックマーク / 2009年11月17日

さっそくどう変わったのか確認したところ、なんともアンビリーバボーな実装になってました。

# List::MoreUtils-0.22
sub uniq (@) {
    my %h;
    map { $h{$_}++ == 0 ? $_ : () } @_;
}

# List::MoreUtils-0.25_02
sub uniq (@) {
    my %h;
    my $ref = \1;
    map { $h{defined $_ ? $_ : $ref}++ == 0 ? $_ : () } @_;
}

新uniq関数では、なにやら「1」のリファレンスを取ってmap中にdefinedでチェックしたり色々処理が増えてます。

何がしたいのかというと旧uniq関数ではハッシュの特性上、未定義値が空文字と同義と見なされてしまい、別々の値としてユニークにすることが出来なかったのでそれに対応するために新uniq関数ではごにょごにょやっているわけなんですね。

実際に挙動を見てみましょう。

use Data::Dumper;

sub uniq_old (@) {
    my %h;
    map { $h{$_}++ == 0 ? $_ : () } @_;
}

sub uniq_new (@) {
    my %h;
    my $ref = \1;
    map { $h{defined $_ ? $_ : $ref}++ == 0 ? $_ : () } @_;
}

my @array = ('','',undef,undef);

print Dumper([uniq_old(@array)]);
print Dumper([uniq_new(@array)]);
$VAR1 = [
          ''
        ];
$VAR1 = [
          '',
          undef
        ];

旧uniq関数の場合、ユニーク結果が空文字だけになってしまってます。新uniq関数だと空文字と未定義値がちゃんと別々のものとしてユニークになってますね。


しかし、この新uniq関数なんですが、少し実装に問題があります。

まず1のリファレンスを持っておき、配列中に未定義値があった場合その1のリファレンスの値を使用してユニーク処理を行ってるわけですが、この時たまたま配列に1のリファレンスの値とまったく同じ文字列が入っていた場合、誤認識してしまいます。ややこしい!

# 配列にリファレンスの値っぽい普通の文字列が入ってる。
# これがもしuniq関数中の\1のリファレンスの文字列と被ってたら誤認識する。
my @a = (undef,'SCALAR(0x1bc77e0)');

print Dumper [uniq(@a)];
$VAR1 = [
          undef
        ];

と、このように「'SCALAR(0x1bc77e0)'」という文字列が消えてしまいます。もちろんこの文字列はたまたま被った場合の話なので環境によっては値が異なります。

これに対応するとなるとこんな感じになりますかね。

sub uniq (@) {
    my (%h,$h);
    map { (defined $_ ? $h{$_} : $h)++ == 0 ? $_ : () } @_;
}

my @a = (undef,'SCALAR(0x1bc77e0)');

print Dumper [uniq(@a)];
$VAR1 = [
          undef,
          'SCALAR(0x1bc77e0)'
        ];

ばっちりですね。

しっかし、何でも汎用的に作ろうとするとどんどん実装が重くなっていきますよね。

このuniq関数にしたって空文字と未定義値を区別したいなんてニーズが果たしてどれくらいあるのかわからないのに汎用的であるが故に実装しなければならないという。

配列か連想配列かを判別する

PHPにおいて、ある配列が連想配列として使われているのかどうかを判別したい場合どうすればいいのか。

色々調べてたらこんな記事を発見しました。

<?php

function is_hash(&$array) {
    return array_keys($array) !== range(0, count($array) - 1);
}
テレパス・ラボ : 連想配列 判定関数(is_hash)

なるほど。

array_keysで配列のキー一覧を取得し、そのキー一覧が0からの連番になっているかどうかを比較すれば、対象の配列が配列なのか連想配列なのかを判別できるという理屈ですね。

確かにこれで判別ができそうです。

ですがひとつだけ気になる部分があって、array_keysやrangeを使って一時的な配列を確保しているのは無駄が多いのではないかと。

対象となる配列が大きければ大きいほどメモリ効率が悪くなりそうな気がします。

要件としては、対象の配列のキーが0からの連番になってるかどうかを調べたいだけなのでforeachでループしながら比較する方が効率良いのではないかと思い、実装してみました。

<?php

function is_hash(&$array) {
    $i = 0;
    foreach($array as $k => $dummy) {
        if ( $k !== $i++ ) return true;
    }
    return false;
}

こんな感じですね。

そして次にベンチマークをとってみます。

<?php
// PHP5.2.8

require_once "Benchmark/Iterate.php";

function is_hash1(&$array) {
    return array_keys($array) !== range(0, count($array) - 1);
}

function is_hash2(&$array) {
    $i = 0;
    foreach($array as $k => $dummy) {
        if ( $k !== $i++ ) return true;
    }
    return false;
}

$range = range(1,1000);  // 実験回数1000回
$array1 = range(0,1000); // 要素1001の配列

function is_hash_a () {
    global $range;
    global $array1;
    foreach($range as $i ) {
        is_hash1($array1);
    }
}

function is_hash_b () {
    global $range;
    global $array1;
    foreach($range as $i ) {
        is_hash2($array1);
    }
}

$bench   =& new Benchmark_Iterate;
$bench->run(1,'is_hash_a'); $result1 = $bench->get();
$bench->run(1,'is_hash_b'); $result2 = $bench->get();

print('is_hash_a -> '.$result1['mean'] . "\n");
print('is_hash_b -> '.$result2['mean'] . "\n");

要素が1001個ある配列を1000回繰り返し実行した場合のベンチマークです。

$ php bench.php
is_hash_a -> 0.265428066254
is_hash_b -> 0.106162071228

foreachで処理する方が2.6倍ほど早くなりました。

他にも色々な条件でやってみます。

// 要素が10個の配列を10万回実行
$ php bench.php
is_hash_a -> 0.319720983505
is_hash_b -> 0.169075965881

要素が少ない場合でもforeachの方が早いですね。

// 要素が30万個の配列を1回実行
$ php bench.php
is_hash_a -> 0.308174133301
is_hash_b -> 0.038535118103

要素が多い場合は速度の差が顕著にでます。

因みに配列の要素を100万個にしてみたら

Fatal error: Allowed memory size of 134217728 bytes exhausted (tried to allocate 524288 bytes)

というエラーがでました。どうやらarray_keysやrangeで確保するメモリの量が多すぎてプログラムが落ちるようです・・・。foreachなら100万個でも難なく処理できます。

で次に連想配列を渡したときのベンチマークですが、foreachの場合、キーが連番じゃなかったり数字じゃなかった時点で処理が終了するのでめちゃめちゃ早くなりました。

試しに文字列のキーを持つ10万要素の連想配列ベンチマークを取ってみます。

// 以下のような連想配列でベンチマークしてみる
// $array1 = array_merge(array('a'=>1),range(0,100000));
$ php bench.php
is_hash_a -> 0.933251142502
is_hash_b -> 1.59740447998E-5

比較にならないぐらい桁違いに早いですね。数万倍くらい?

結論としては

  • 配列の大きさに関わらずforeachの方が早い
  • 配列が大きければ大きいほどforeachの方が更に早い
  • 対象の配列が連想配列だった場合、foreachの方が桁違いに早い。

という感じなりますか。

foreach万歳ですね。ただひとつだけデメリットをあげるならば、ワンライナーじゃないというところになりますか^^;

さてこれでカンペキ!と言いたいところですが、実は実際のところ、連想配列として利用する配列のキー名に0という数字は使わないという前提条件がある場合、その対象配列の最初のキーが0かどうかだけをチェックすればそれだけでOKだったりするんですよね。

<?php

function is_hash_lite(&$array) {
    reset($array);
    list($k) = each($array);
    return $k !== 0;
}

こんな感じ。もちろん速度的にはピカ一で早いです。

厳密にチェックしたい場合はforeachを使い、前提条件がある場合は最初のキーだけをチェックする感じで良いんじゃないでしょうか。

ArrayObjectによる配列演算子のオーバーロード風

PHP5やり始めたばっかでまだ右も左もわからずに右往左往してます。

色々調べてたら、PHP5になってSPL(Standard PHP Library)ってのが導入されたということを知った。へー。

そのSPLの中にArrayObjectという物がありまして、オブジェクトを配列っぽく扱えるようにしたものなのですよ。今日はコレを取り上げてみたいと思います。

とりあえず使用例。

<?php

$foo = new ArrayObject;

$foo['foo'] = 100;
$foo['foo-bar'] = 200;

print_r((array)$foo);
Array
(
    [foo] => 100
    [foo-bar] => 200
)

おお、面白い。オブジェクトなのに配列のように扱えています。

そして更に面白いのがオブジェクトのプロパティに設定した値と同期させることもできるところです。

以下の例を見てください。

<?php

$foo = new ArrayObject(array(),ArrayObject::ARRAY_AS_PROPS);

$foo->foo = 9999;      // プロパティfooに設定
$foo['foo-bar'] = 200; // 配列形式でfoo-barに設定

print_r((array)$foo);
Array
(
    [foo] => 9999
    [foo-bar] => 200
)

これは凄いですね。

ある変数に対して、プロパティ呼び出しでも配列形式でもどちらの方法でもアクセスできるというのはかなり便利です。

記号等を含まないデータ名の場合はプロパティ呼び出しで値を設定し、含んでしまうようなデータ名の場合は配列形式で値を設定するという感じでしょうか。

<?php

// 普段はプロパティでアクセスする
$stash->foo = 1;
$stash->bar = 22;
$stash->baz = 44;
$stash->hoge = 55;

// どうしても記号を含む場合は配列でアクセスする
$stash['foo-bar'] = 200;
$stash['baz::hoge'] = 300;

僕の場合、オブジェクトのプロパティを連想配列代わりとして使うことがちょくちょくあったりするんですよね。やっぱ括弧やシングルクォートすら書くのが面倒臭いので->だけでアクセスできると凄い楽なんです。当然、記号が必要になりそうなケースでは初めから連想配列を使いますが。

このArrayObjectはPerlCatalystのstashの様な使い方に向いてるんじゃないかなぁと思います。

個人的に非の打ち所が無いArrayObjectですが、こうなるとやっぱり気になるのは速度面でどうなのか?ってことですよね。

というわけでArrayObjectとstdClassと配列でベンチマークをとってみました。

<?php

require_once "Benchmark/Iterate.php";

function array1 () {
    $foo = new ArrayObject(array(),ArrayObject::ARRAY_AS_PROPS);
    foreach(range(0,10000) as $i ) {
        $p = "aaa$i";
        // プロパティ、配列2パターン試しておく
        $foo[$p] = 10;
        $foo->$p += 10;
    }
    $array = (array)$foo;
}

function array2 () {
    $foo = new stdClass;
    foreach(range(0,10000) as $i ) {
        $p = "aaa$i";
        $foo->$p = 10;
        $foo->$p += 10;
    }
    $array = (array)$foo;
}

function array3 () {
    $foo = array();
    foreach(range(0,10000) as $i ) {
        $p = "aaa$i";
        $foo[$p] = 10;
        $foo[$p] += 10;
    }
    $array = (array)$foo;
}

$counter =  10;
$bench   =& new Benchmark_Iterate;
$bench->run($counter,'array1'); $result1 = $bench->get();
$bench->run($counter,'array2'); $result2 = $bench->get();
$bench->run($counter,'array3'); $result3 = $bench->get();

print('arrayobject -> '.$result1['mean'] . "\n");
print('stdclass    -> '.$result2['mean'] . "\n");
print('array       -> '.$result3['mean'] . "\n");
arrayobject -> 0.032885
stdclass    -> 0.033031
array       -> 0.031132


え、ちょ、ま。

殆ど同じじゃないっすか!

あれれ、ベンチの書き方間違ってるのかな・・・。あまりPHPでベンチ書かないからよくわからない。

しかし、もし本当にこれだけ早いのならコレマジで使わない手は無いですね。便利すぎる。

PHPでnewと同時にメソッドチェーンする方法

最近ようやくPHP5をちょこちょこと触ってるわけなんですが、折角メソッドチェーンサポートされたのに何故かnewと同時にできない罠。

これなんか動いても良さそうなのになぁ。直感的だし。

<?php

(new Foo)->bar();

嘆いてもしょうがない。動かないものは動かないので、ちょこちょこと調べてたらこんなのが

そこで以下のような関数を定義する。

<?php
function ref($obj)
{
    return $obj;
}

この関数はみたとおり渡された値をそのまま返す関数だが、これを使うことで以下のようにメソッドチェーンできるようになる。

<?php
ref(new Hoge)->doSomething();

というバッドノウハウっぽい話。

PHPでコンストラクタからメソッドチェーンする方法 - id:anatooのブログ

やっぱこれくらいしかないよねー。

でも標準関数だけでやりたいのが人情というもの。あれこれ試してみたところ、やっとこさ実現できた。

ってことで僕が出した結論はコレ。

<?php

pos(array(new Foo))->bar();

これがホントのバッドノウハウ

可読性?しったこっちゃねーお!