たきゃはしです

Webとかデザインとかプログラムとか

CakePHP h()メソッドから PHP を学ぶ


Cake使いなら誰もが体験するというh()メソッド


Cakephp Advent Calendar 2012/24 担当の高橋(@)です!
Cakeはソースコードも楽しいですよ!という記事です。


この記事の内容はフレームワーク入門者レベルです。
そんでもって、↓このあたりがサクッと学習できます。

三項演算子
・staticキーワード
再帰呼び出し
・マジックメソッド(__toString()メソッド)

超変態マニアック変態プログラマーはリツイートをお願いします。


オープンソースのコードって何かキッカケがないと読む機会がないんですよね。
Cakeはドキュメントが充実してるので必ずしも読む必要があるという訳ではないですが、読んでみると面白いもんなのでぜひこれを機会にOSSを体験してください。


というわけで、CakePHPの h()メソッド を解説していきます。
ちなみに h()メソッドは、サニタイズの役割を持つエスケープ関数です。


まずは私が過去に作ったh()メソッドがこちら(赤面)

function h($string, $flags = ENT_QUOTES, $encoding = 'UTF-8') {
    return htmlspecialchars($string, $flags, $encoding);
}

あー、なるほどー。という感じですね。

これほど単純なメソッドでもCakeだと・・・

function h($text, $double = true, $charset = null) {
    if (is_array($text)) {
        $texts = array();
        foreach ($text as $k => $t) {
            $texts[$k] = h($t, $double, $charset);
        }
        return $texts;
    } elseif (is_object($text)) {
        if (method_exists($text, '__toString')) {
            $text = (string) $text;
        } else {
            $text = '(object)' . get_class($text);
        }
    } elseif (is_bool($text)) {
        return $text;
    }

    static $defaultCharset = false;
    if ($defaultCharset === false) {
        $defaultCharset = Configure::read('App.encoding');
        if ($defaultCharset === null) {
            $defaultCharset = 'UTF-8';
        }
    }
    if (is_string($double)) {
        $charset = $double;
    }
    return htmlspecialchars($text, ENT_QUOTES, ($charset) ? $charset : $defaultCharset, $double);
}

となります。


「あーなんか難しそう。無理無理」
と思われた方もいるかもしれませんが、ご安心を。
実はとても分かりやすいコードです。


また冒頭でも述べたように

三項演算子
・staticキーワード
再帰呼び出し
・マジックメソッド(__toString()メソッド)

このあたりの技術を得ることも出来ます!
興味のある方はこのまま読み進めてください!

※Cakeのhメソッドはlib/Cake/basic.phpの165行目付近にあります


まず最後を読んでみる

    return htmlspecialchars($text, ENT_QUOTES, ($charset) ? $charset : $defaultCharset, $double);

最後に「htmlspecialchars()メソッド」が使われています。
つまり道中はさておき、エスケープ処理で終わるという事が分かりました。

第二引数に見慣れないコードがありますが、これは「三項演算子」といいます。
三項演算子とは、一言でいえばif文を省略する書き方です。
よって↓の2つは同じです。

//三項演算子
return ($charset) ? $charset : $defaultCharset

//if 文
if ($charset) {
    return $charset;
} else {
    return $defaultCharset;
}

三項演算子を使うと1行に収まるのでスマートですね。
ただし、ロジックは書かないように気をつけましょう。

ところで $charset と $defaultCharset はどこから来たのでしょうか。
直前のソースコードを読みます。

    static $defaultCharset = false;
    if ($defaultCharset === false) {
        $defaultCharset = Configure::read('App.encoding');
        if ($defaultCharset === null) {
            $defaultCharset = 'UTF-8';
        }
    }
    if (is_string($double)) {
        $charset = $double;
    }

$defaultCharset

static $defaultCharset = false
静的変数として定義されていました。

if ($defaultCharset === false) {
必ず真となるためスコープへ入ります。

$defaultCharset = Configure::read('App.encoding');
これはCakeで設定されている文字コードを参照しています。(初期値は'UTF-8'です

if ($defaultCharset === null) {
$defaultCharset = 'UTF-8';
}
仮に設定が nullであった場合でも自動的に'UTF-8'が設定されるようになっています。

また defaultCharset は静的変数なのでスコープ外でもクリアされません。
したがって後述する再帰処理でも無駄な処理を実行しません。

http://php.net/manual/ja/language.variables.scope.php#language.variables.scope.static

$charset

    if (is_string($double)) {
        $charset = $double;
    }

$charset と $double はh()メソッドの引数でしたね。
お互いに'UTF-8'のような文字コードを期待しているようです。
どちらかに設定すれば文字コードを上書き出来るようです。

if ($charset) {
    return $charset;
} else {
    return $defaultCharset;
}

つまり、文字コードはh()メソッドの引数$charsetか$doubleの設定を優先するが
指定がない場合はCakeで設定した文字コードを使用する。という事ですね。

これで、returnで返される第二引数の正体が判明しましたね!


これで半分!
続けて始めの部分を読み解きましょう。

function h($text, $double = true, $charset = null) {
	if (is_array($text)) {
		$texts = array();
		foreach ($text as $k => $t) {
			$texts[$k] = h($t, $double, $charset);
		}
		return $texts;


ん、なぜ初っ端に is_array($text) なんだろう。
と思われた方・・・エクスタシー!!!!!

そう!最初の処理は、エスケープの対象が文字列でなく
配列だった場合の処理なんです!

もし、$textが配列だった場合、新たな変数$textsを空の配列とする。
そして $textは配列なので foreach でループして h()メソッドを実行する。
すると、また中身が配列だとしても2次元、3次元と全て処理する事が出来るのです。
これを「再帰呼び出し(recursive)」といいます!


if文はまだ続きます。

    } elseif (is_object($text)) {
        if (method_exists($text, '__toString')) {
            $text = (string) $text;
        } else {
            $text = '(object)' . get_class($text);
        }
    } elseif (is_bool($text)) {
        return $text;
    }

文字列でも配列でもなく、オブジェクトだった場合の処理もあります。
ここでは何をやっているかと言うと

        if (method_exists($text, '__toString')) {
            $text = (string) $text;
        } else {
            $text = '(object)' . get_class($text);
        }

オブジェクト(クラス)に __toStringメソッドが存在していた場合、そのまま文字列として返します。
__toStringメソッドがない場合はクラス名を返します。

__toString とはマジックメソッドでクラスオブジェクトが文字列として評価された時の値を設定します。
逆にクラスオブジェクトはそのまま文字列として評価すると深刻なエラーで処理が中断しますので注意してください。

マジックメソッドは他にもたくさんあるので、見てみてくださいね。

php.net/manual/ja/language.oop5.magic.php


残りは

    } elseif (is_bool($text)) {
        return $text;
    }

エスケープ対象が true か false だった場合はそのまま返すという処理です。

以上です!お疲れさまでした!
エスケープでここまでやるなんて、とても驚きました。そして美しい上にテクっているコードには感動を覚えてしまいます。


こんな感じで、Cakeに限らずフレームワークなどオープンソースのコードを読む事は
新しい発見やスキルアップにも繋がるエンジニアにとって重要な探求でもあると考えています。


みなさんが見つけた面白いコードがありましたら、ぜひ教えてください。