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を使えば本件は解決するようです(まだ試してない)。とはいえ、すぐ対応できる環境ばかりではないだろうから、悩ましい問題であることは変りないのですが。