CakePHPでラジオボタンを出そうと思ったら、PCREとかPHPのビルドを考え直すハメになった

(2011/05/18追記)末尾にCentOSの標準RPMの更新について書きました

(2011/05/19追記)続編書きました CentOS5.5でCakePHP1.3系のInflector::slugを正常動作させる方法

CakePHPのFormHelperでラジオボタンを出してみたところ、label要素がうまく動かない症状に遭遇しました。で、調べていくと斜め上な展開を見せたのでブログにまとめてみる。

検証環境はCentOS5.5 x86_64、PHPは5.2.12(自前RPM)です。CakePHPは2011/05/18現在のgithubのmasterで確認。

ラジオボタンがうまく選択できない

CakePHPのFormHeplerでラジオボタンを出そうとして、http://book.cakephp.org/view/1429/radioのサンプルを動かしてみたところ、label要素が思うように動作しない現象を確認。

<?php
$options=array('M'=>'Male','F'=>'Female');
$attributes=array('legend'=>false);
echo $this->Form->radio('gender',$options,$attributes);
?>

出力されたHTMLを見てみるとこんな感じ。

<input type="radio" name="data[Hoge][gender]" id="HogeGender" value="0" checked="checked"  /><label for="HogeGender">Male</label>
<input type="radio" name="data[Hoge][gender]" id="HogeGender" value="1"  /><label for="HogeGender">Female</label>

input要素のidの振られ方に問題があるらしい。

ということで、cake/libs/view/helpers/form.phpを見てみると、radioメソッドで以下のようにidを決定していることが確認された。

<?php
1094       $tagName = Inflector::camelize(
1095         $attributes['id'] . '_' . Inflector::slug($optValue)
1096       );

■Inflector::slug

Inflector::slugを通すとダメになるようなので、cake/libs/inflector.phpを確認。

<?php
617     $merge = array(
618       '/[^\s\p{Ll}\p{Lm}\p{Lo}\p{Lt}\p{Lu}\p{Nd}]/mu' => ' ',
619       '/\\s+/' => $replacement,
620       sprintf('/^[%s]+|[%s]+$/', $quotedReplacement, $quotedReplacement) => '',
621     );

この1つ目の正規表現がうまく働いていないようだ。パターンにはUnicode文字プロパティが使用されていて、このへんが怪しい。詳細は以下。

調べてみると、CentOS標準のPCREを用いてPHPをビルドすると、CakePHP1.3系でこの問題が発生するらしい。


#582 function slug in inflector.php - CakePHP - cakephp」でこの件に関する議論が確認できるのだが、「それお前のPCREがぶっ壊れてるだけだから」の一言で終了している。

手元の環境で確認してみると、確かにこれが原因のようだ。

$ pcretest -C
PCRE version 6.6 06-Feb-2006
Compiled with
  UTF-8 support
  No Unicode properties support
  Newline character is LF
  Internal link size = 2
  POSIX malloc threshold = 10
  Default match limit = 10000000
  Default recursion depth limit = 10000000
  Match recursion uses stack

utf-8が有効な場合に、「No Unicode properties support」だとダメらしい。

1.2では問題なくて、1.3でUnicode文字列に対応させた際にこの問題が発生したようだ。

Inflector::slugがどの程度使われているのかを確認したところ、現在使用中のバージョンでは以下の結果が得られた。

$ find cake -name '*.php'|xargs grep 'Inflector::slug'|grep -v 'cake/tests'|sort
cake/console/libs/tasks/model.php:                              $validatorName = Inflector::slug($choice);
cake/console/templates/skel/config/core.php: *          'prefix' => Inflector::slug(APP_DIR) . '_', //[optional]  prefix every cache file with this string
cake/console/templates/skel/config/core.php: *          'prefix' => Inflector::slug(APP_DIR) . '_', //[optional]  prefix every cache file with this string
cake/console/templates/skel/config/core.php: *          'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string
cake/dispatcher.php:                    $path = strtolower(Inflector::slug($path));
cake/libs/cache/apc.php:                parent::init(array_merge(array('engine' => 'Apc', 'prefix' => Inflector::slug(APP_DIR) . '_'), $settings));
cake/libs/cache/memcache.php:                   'prefix' => Inflector::slug(APP_DIR) . '_', 
cake/libs/cache/xcache.php:                     'engine' => 'Xcache', 'prefix' => Inflector::slug(APP_DIR) . '_', 'PHP_AUTH_USER' => 'user', 'PHP_AUTH_PW' => 'password'
cake/libs/model/cake_schema.php:                        $this->name = Inflector::camelize(Inflector::slug(Configure::read('App.dir')));
cake/libs/view/helpers/cache.php:               $cache = strtolower(Inflector::slug($path));
cake/libs/view/helpers/form.php:                                                        $this->model() . '_' . $this->field().'_'.Inflector::slug($name)
cake/libs/view/helpers/form.php:                                $attributes['id'] . '_' . Inflector::slug($optValue)
cake/libs/view/view.php:                                $cacheFile = 'element_' . $key . '_' . $plugin . Inflector::slug($name);
cake/libs/view/view.php:                                $key = Inflector::slug($params['cache']['key']);
cake/libs/xml.php:                      $name = Inflector::slug(Inflector::underscore($name));
cake/libs/xml.php: *   to have its tags run through Inflector::slug().  Defaults to true

■対策

  1. PCREを「--enable-utf8 --enable-unicode-properties」でビルドして、PHPRPMも作り直す
  2. inflector.phpのslugメソッドを直接書き換える
  3. radio()に渡す$optionsのキーを適当に水増ししてslugの結果をユニークにする

1番が正当な解決策なのだろうが、ラジオボタンを動作させるためのコストとしては巨大すぎる。アプリケーションの他の部分に影響が出る可能性も含めて、出来れば避けたい。現時点で正常動作していない可能性は十分にあるのだが、そこも含めて検証するだけの時間をかけたくない。

2番がわりと妥当な気がして、slugメソッドだけ1.2から持ってくる、あるいは正規表現のパターンを変更するというのがわりとよさそう。元は1.2で動いていたアプリケーションなので、そこで問題が出る可能性が低いというのもある。

3番は後からvalueを直す必要があったり、対応として場当たり的すぎるが「このラジオボタンをどうにかしたい」という一点に絞れば影響範囲は最小。

■コアモジュールの置き換え

現在の状況から、inflector.phpを差し替えることが適当と思われたので、具体的な方法を模索してみた。cakeディレクトリ以下のファイルを書き換えるのは対応として筋が悪すぎるので避けたい。バージョンアップしたらそこで試合終了ですよ。

リポジトリには、app/libsという思わせぶりなディレクトリがあって、ここにファイルを置いたら優先して読んでくれるのではと期待したがそんなことはなかった。

app/webroot/index.phpがcake/bootstrap.phpを呼んでいて、その中で固定パスでinflector.phpその他をrequireしているだけで、ここをプラガブルに運用出来る仕組みは提供されていない。

Cake使う時には「何かあったらコアの挙動を変えればOK」という空気感らしいが、その時のベストプラクティスは無いものだろうか…

cake/bootstrap.phpの処理自体はシンプルなので、わざわざ呼ばずにapp/webroot/index.phpから同等+αの処理を直接書いてやるというのも一つの手か。

どうにも「これでいいのだ」と思えるすっきりした解法が見当たらない。

■まとめ

PHP4をサポートしているのに、普及率の高いディストリビューションであるCentOSのPCREは切り捨てるというのは納得がいかない。「誰でも堅牢なアプリケーションがRapidに書けるよ」って売り文句のフレームワークがPCREのリビルドを要求するってどうなの。

ラジオボタンに適切なlabelを振りたいと思ったらPCREのビルドが必要というのも、問題の芋づる感がひどい。

CakePHPを使っている人が全員PCREを自分でビルドしているとは到底思えないのだが、皆どうしているのだろうか…

■追記:CentOS標準のRPMが更新されていた件について

SRPMからSPECを取り出して覗いてみたところ、

 50 %build
 51 %configure --enable-utf8 --enable-unicode-properties

となっていることを確認しました。ChangeLogには

102 * Wed Jul 21 2010 Petr Pisar <ppisar@redhat.com> - 6.6-6
103 - Enable Unicode properties (Resolves: #457064)

とあるので、7月の時点でコンパイルオプションが修正されたようです。cobblerでミラーしてバージョン固定してたから気付かなかった。

結果論ではあるが、最新のPCREを使えば本件は解決するようです(まだ試してない)。とはいえ、すぐ対応できる環境ばかりではないだろうから、悩ましい問題であることは変りないのですが。

ip_conntrack_maxの設定値を監視するNagiosプラグインを書いた

どうというものでもないが、せっかくだから記録として残しておこう。NRPE経由での利用を前提としています。

iptablesの設定を変更するのに、/etc/sysconfig/iptablesを更新して

$ sudo /etc/init.d/iptables restart

で適用すると、

$ cat /proc/sys/net/ipv4/ip_conntrack_max
65535

とかリセットされて、sysctl -pで再設定するのを忘れてパケットさばききれなくなるとか、悲しい事態に陥ることがあるのだけど、もう少しましなソリューションはないもんでしょうか。

CakePHPで__(0)するとNULLが返ってくる

現時点のgithubのmasterでcake/bascis.phpから。

function __($singular, $return = false) {
	if (!$singular) {
		return;
	}

これはひどい

is_nullとか===とか、ちゃんと使わないと残念なバグを仕込んでしまうという、PHPにおける基本的な教訓を改めて得られますね。

CakePHPのControllerがディレクトリの階層に対応していた件

(追記)本記事の内容はCakePHP1.3.6ならびに2011/04/28時点でのgituhbの1.2, 1.3ブランチで検証しました。

(2011/05/05追記)コメントで現時点のCake2.0では再帰的な探索を行わないとのご指摘を頂きました。ちゃんと追えてないけど、import周りの処理は大幅に変更されるようです。

CakePHPではControllerは1つのディレクトリに全部入れるのがふつう」という話を聞いて、さすがにそれはないだろうと思って調べたらapp/config/bootstrap.phpに$controllerPathsを書けば探索パスを追加できるという情報を得た。

のだけど、どうにもうまく動かなくてあちこちいじくり回してたらいつの間にか動くようになったり、別のサーバにdeployしたらまた動かなくなったりして挙動が怪しい。

本体のコードを見ると、$controllerPathsなんてどこにも書いてなくて何かが間違っているように見える。

によれば1.3になった時点で$controllerPathsは使えなくなったらしい(それならそれでwarningなりと吐けよと思うのだが)。

で、$controllerPathsが動作することを期待したコードは動かないはずなのだが、目の前には何故か動く環境と動かない環境があって意味がわからないよ。と思ってコードを追ってみるといろいろわかった。

■App::importの処理

App::importはファイルを探索する際の対象パスとして、file_mapキャッシュ(app/tmp/cache/persistent/cake_core_file_map)を参照する。このキャッシュは1行目が有効期限、2行目がシリアライズしたデータになっており、以下のコードを実行することで内容を確認できる。

php -r '$c=explode("\n",file_get_contents("app/tmp/cache/persistent/cake_core_file_map"));var_dump(unserialize($c[1]));

以下はApp::importがControllerを読み込む時のあるユースケースにおけるフローを読んだメモ。

  • file_mapに入ってるなら読み込みを試みる
    • 成功したらそこでreturn
    • 失敗したらfile_mapキャッシュの該当キー削除を予約する
  • file_mapに無い場合はあれこれ通って、App::__findがファイルを探す
    • controllerの探索パスをdir_mapキャッシュ(app/tmp/cache/persistent/cake_core_dir_map)から取得
    • dir_mapにキーが無ければディレクトリを「再帰的に」たどって探索パスのリストを作る($this->__pathsに入れる)
    • 作った探索パスのリストから、Controllerの読み込みを試みる
      • 読み込みに成功したらキャッシュの更新を予約する

ざっくりとした流れは「file_map探す → dir_mapをキャッシュから取る、無ければ作る → dir_mapからファイル探索」といったところ。

ここで問題になるのが、dir_mapが正しいことを誰も保証してくれないこと。dir_mapにキーがなければディレクトリを読んでくれるが、古いデータがdir_mapに入っていた時にそれを更新する仕組みがない。

■経緯の妄想

ここから先は調べるのが面倒なので、憶測だけ書き散らかす。誰か詳しく知っていたら教えてください。

  • ある時点まではApp::__findはディレクトリを再帰的にたどる機能を持っていなかった
  • なので、dir_mapが更新されるのはアプリケーションの設計が大きく変わる時だけだった
  • 「それぐらい変わる場合はキャッシュを手動で消せよ」で済んでいた
  • ある時からApp::__findがrecursiveをサポートするようになった
  • dir_mapが更新されるべきケースが出てきた
  • 面倒なので、あるいはそういうケースは特殊なので放置している

App::__findでControllerも再帰的に探索できるという情報が広まっていないようなので、この機能はCake的には位置付けとして重要でないのかも知れない。

そもそも、この機能はドキュメントに載っていないんじゃないかと思ったが、あらためて探してみると、公式ドキュメントの「2.4.1 ファイルとクラス名の規約」に

各ファイルは、 app フォルダ内のそれぞれ適切なフォルダの中かその下(サブフォルダも可)に設置します。

とオマケ程度に触れられているのが確認できた。


このdir_mapが更新されない問題は、1ヶ月ぐらい前の2.0のブランチでも変わっていなかったので、このあたりは「お前がキャッシュ消せば済む話」という扱いなのかも知れない。最新版ではコードの構成が根本的に変わっていたので追いきれていない。

■まとめ

  • Controllerをサブディレクトリに配置する機能は標準でサポートしている
  • ディレクトリ構造を変えた時は、dir_mapのキャッシュを消す必要がある

CentOSでRealTekRTL8111bにr8168ドライバを適用する方法

2年ぐらい前のネタだが今でも解決してないので、時期を外しても何ともないぜ。

RealTekのRTL8111bという2年程前(LGA775世代)から広く使われているNICがあります。こいつのドライバはr8168が正しいのですが、CentOS5系で自動認識させるとr8169がロードされてしまうという問題があります。

r8169でも動いたりするのですが、ifconfigなどで見るとdroppedが増えていたりして危険なことがあります。一時的には動いても、ある日突然転送エラーを吐き始めて、ついには疎通しなくなったりすることも経験しています。数ヶ月の安定運用の後いきなり止まったりすることもあるので、油断出来ません。

この問題の詳細については、CentOS Wikiから参照出来ます。CentOS5.2から発生した問題のようですが、CentOS5.5でも相変わらずなのが困りもの。

NICの型番とドライバの確認

RTL8111bが使われているかは、例えば以下のようにして確認できます。

$ /sbin/lspci |grep -i realtek
01:00.0 Ethernet controller: Realtek Semiconductor Co., Ltd. RTL8111/8168B PCI Express Gigabit Ethernet controller (rev 02)

こういうのが引っかかってきたらドライバを確認します。

$ /sbin/lsmod|grep r81

これでr8169が出てきたら気をつけた方がいいかもしれません。

■r8169の導入

ドライバを入れる方法はいろいろありますが、ELRepoからkmod-r8168を入れるのが便利です。

1. ELRepoを有効に
$ sudo rpm -Uvh http://elrepo.org/elrepo-release-0.1-1.el5.elrepo.noarch.rpm
2. ELRepoから上記のkmod-r8168をインストール。
$ sudo yum --enablerepo=elrepo install kmod-r8168

中身を少し見てみましょう。

$ rpm -ql kmod-r8168
/etc/depmod.d/r8168.conf
/etc/modprobe.d/blacklist-r8169.conf
/lib/modules/2.6.18-128.el5
/lib/modules/2.6.18-128.el5/extra
/lib/modules/2.6.18-128.el5/extra/r8168
/lib/modules/2.6.18-128.el5/extra/r8168/r8168.ko
/usr/share/doc/r8168-8.019.00_NAPI
/usr/share/doc/r8168-8.019.00_NAPI/GPL-v2.0.txt
/usr/share/doc/r8168-8.019.00_NAPI/README

$ cat /etc/depmod.d/r8168.conf
override r8168 * weak-updates/r8168

これで/lib/modules/2.6.XX-XXX.X.X.el5/weak-updates/r8168にsymlinkを貼って読ませるようです。kernelをアップデートしてもドライバを引き回せて非常に安心感があります。r8169をブラックリスト登録してくれるのも気が利いてますね。

3. /etc/modprobe.confのeth0(適宜読み替えのこと)の設定を以下に。
alias eth0 r8168
4. モジュールの入れ替え

1コマンドずつ入力していると、rmmodした時点でネットワークが切断されてコンソールが必要になってしまいます。

一発実行してしまえばネットワーク越しに入れ替えが出来るのでお勧め。

$ sudo /sbin/rmmod r8169 && sudo /sbin/depmod -a && sudo /sbin/modprobe r8168 && sudo /sbin/service network restart

これで作業は完了です。最初のように、

$ /sbin/lsmod|grep r81

として、r8168がロードされていることを確認しましょぅ。

「ソフトウェア設計とは何か?」がすごい

結構前のエントリになりますが、cles::blogさんで紹介されている「プログラミングは設計か製造か?」に感銘を受けました。はてブを見ていると、最近になってwebarchiveから発掘されたようです。

原文はこちらで公開されている模様。

全編にわたって非常に示唆に富んだ内容となっています。印象深かったトピックは以下。

  • ソースコードは設計であり、ドキュメントである
  • ソフトウェア開発における「製造」とはビルドである
    • 製造はコンパイラとリンカの仕事であり、コストは非常に小さい
  • テストやデバッグは設計の検証と洗練のプロセスである
    • 他の工学分野のそれと等価で手を抜くべきでない
  • 「コーディング」「テスト・デバッグ」「(俗に言う)設計」は全て設計の一部である
  • 上位設計・下位設計は密接に関わっており、同程度に重要である
  • 上位設計の意図を伝えられる、表現力に優れたプログラミング言語が必要である

アジャイル開発、XPといった潮流の中で近年さかんに提唱されてきた概念が、今から20年近く前に既に提唱されていたことに驚きと感銘を受けます。

更に言えば、ソフトウェアのビルドを「製造」と位置付け、上位設計からコーディングまで全てを「設計」とする視点は今でも非常に新鮮なものに思われます。

この論によれば、LLに代表されるインタプリタ型の言語で一定規模のアプリケーションが作成可能となった現在では、ビルドのコストは安価であるどころか意識することさえありません。また、それらは擬似コードと同等のレベルで抽象化されたコードが書けるだけの柔軟な言語仕様を持っています。テストを書いたり実行する環境も当時とは比較ならない程の進化を遂げているはずです。

これだけ恵まれた環境にある我々がソフトウェア設計について甘い認識を持ったままコードを書いたり、テストやリファクタリングから目を背けることはソフトウェアに対して失礼なのではないかとさえ思えてきます。ソフトウェア設計をなめるな。


僕がこの文章を読んで真っ先に頭に浮かんだのは、よしおかさんの「20年前でも当たり前にテストを書く文化はあった」という言葉です。インターネットの発達によって我々が得ることの出来る情報は爆発的に増えましたが、インターネットが普及する以前の情報というのは実はそれほど充実していません。ネットに存在する「当時の生の声」はもっと評価されていい。

さて、文中には「昔々,私たちはウォーターフォール・プロセスを実践していました」ともあります。今は何年でしたっけ。