php Archive
CakePHPとRuby on Railsの違い
最近、仕事でRailsを使い始めたので、今までよく使っていたCakePHPとどこが一緒でどこが違うのかをざっくりまとめてみました。まだRailsは勉強中なので、理解が不十分だったり間違っている箇所もあるかと思いますが、それらの点についてはコメントなどでご教授いただければ幸いです。
Controller
CakePHPの場合、任意のアクションにおいて、/users/show/katsuma のように、URLで「/」で区切られているものは、アクション以降の文字列も勝手に引数に分けてくれます。なので、アクション側の定義で
function show($id, $name)
のように引数を分けて定義しておいてあげれば、勝手に値が割り当てられることになります。
Railsの場合はconfig/routes.rbで振り分け方法を定義しておいてあげる必要があります。上の例だと
map.connect 'users/show/:id/:name', :controller => 'users', :action => 'show'
こんな感じになるでしょうか。
一方で、Railsの場合、routes.rbはものすごい強力で、map.connetのかわりに好きな名前のメソッドを指定するだけで「(名前)_url」で指定したcontroller, actionなどを含むURLを作成することなんかできたりします。たとえば
ActionController::Routing::Routes.draw do |map| map.berryz '', :controller => "berryz", :action=> "show" end
こんな感じにroutes.rbで定義しておいた上で、
berryz_url(:id => 'miyabi')
で呼び出すと、
http://localhost:3000/berryz/show/miyabi
を意味することになります。Railsすごい。。。!
また、GETで引数指定で渡ってきたも、routes.rbで指定された引数も、すべて param[:hoge]のシンボル指定で取得できるのも特徴でしょうか。
ApplicationController
CakePHPの場合、任意のControllerクラスの継承元であるApplicationControllerは "app/" ディレクトリ直下に "application_controller.php" の名前で設置されます。
Railsの場合は、"app/controllers/" の中に "application.rb"の名前で設置され、その場所と名前が微妙に異なります。
この名前については、これ規約からも外れてるよなぁ。。と思ってたら、どうやらRails 2.3.0からは"application_controller.rb"に名前が変わるようですね。詳しい話はこちらに書いてました。(参照元:そういえば ApplicationController ってファイル名の規約を守ってなかったんだな)
View(レイアウト)
CakePHPの場合、大枠のレイアウトはviews/layouts/default.ctp に、そのレイアウトのHTMLを記述します。
Railsの場合は、views/layouts/application.rhtmlに記述することになり、その名前は異なります。統一性の観点から言うと、Railsのこの名前の方が個人的には好きです。
部分テンプレート
CakePHPの場合、部分テンプレート(element)は、views/elements/ 以下に header.ctp の名前で保存しておきます。elementの呼び出す場合は、View側で
$this->element('header')
の、ように呼び出します。
Railsの場合は、特定のcontroller内での共通テンプレート、全controllerでの共通テンプレートとそれぞれ別に分けることができます。 前者の場合は、 views/user/_header.rhtml のように、"views/controller名/_{部分テンプレート名}.rhtml"の形式になります。 逆に後者の場合、views/shared/_header.rhtmlのように、"views/shared/_{部分テンプレート名}.rhtml" の形式になります。
部分テンプレートの呼び出し方は、前者の場合は、<%=render :partial=>'header'/> のように、後者の場合は :partialで指定する値が"shared/header"のようになります。
この点については、Railsはここまで細かい指定がなくてもいいのにな、、と思います。Cakeの方が直感的な規約だし、呼び出し方も簡単かな、と。
Filter
Cake,Railsともにcontrollerにおいて、その前後にフィルタをかけることが可能です。いわゆるbeforeFilter, afterFilterですね。 Cakeでは特定のController、またはApplicationControllerにおいてbeforeFilter/afterFilterアクションを定義しておくことで、そのフィルタを通すことが可能です。
Railsの場合、フィルタにはメソッドはもちろんですが、クラス、ブロックの3つのレベルで指定可能です。また、複数のフィルタが定義されている場合は、その定義された順番にフィルタが適用されます。
このRailsの細かな指定は凄い、としか言いようがないかんじ。特にブロックで渡すことができる柔軟性は使いこなせばすごく便利そうな印象です。(まだ自分はそこまで使いこなせてません)
静的ファイル
CakePHPの場合、画像やCSSファイルなど、静的ファイル(や、routes.phpのルーティングに外れるもの)は、"webroot/" 以下に設置しておけばOKです。
逆に、Railsの場合は、"public" ディレクトリに設置することになり、そのディレクトリ名が異なっています。
これについては、Railsを意識しまくったCakeとしては、どうしてここの名前だけ変えたのかはよく分かりません。。。
まとめ
ざっと目につきやすい相違点をまとめてみました。Railsについてはまだまだ触り始めたばかりなので、相違点はまだまだあるでしょうし、注意して理解を深めて行きたいと思います。 また、今回こうやって相違点を考えていくことで、むしろお互いの理解が深まるんじゃないかな、とも思っています。
ちなみに、チュートリアルについて、book.cakephp.orgは日本語化されてるわけですが、guides.rubyonrails.orgは日本語化されてないんでしょうか?? すごくよくまとまってそうなので、できれば日本語で読んでみたいのですが。。
CakePHPでモデルキャッシュを利用する
Cakeでキャッシュ周りの調査をしていたら、モデルのメソッドの実行結果をキャッシュさせるbehaviorがあるのを見つけました。
これが相当いい感じなので、その利点や導入方法についてまとめておきたいと思います。
コントローラのスリム化
MVCモデルでキャッシュを利用しようという話になると、大抵Controllerでキャッシュヒットの有無を確認して、ヒットしない場合キャッシュをリセットする、というロジックがまず頭に浮かぶと思います。
if (($posts = Cache::read('posts')) === false) {
$posts = $this->Post->find('all');
Cache::write('posts', $posts);
}
ただ、コントローラで毎回このようなキャッシュヒットを確認していると同じようなコードがあちこちに散らばることになるので、保守性が悪くなります。なので、こういうキャッシュ周りの処理はできるだけモデルに振ってしまったほうがよいです。また、基本的なフロー以外の余計なロジックを考えなずにくてよいので、可読性も格段に上がると思います。
// キャッシュヒットすればキャッシュから、ヒットしない場合はDBアクセス
$posts = $this->Post->find('all');
Paginationもキャッシュできる
CakePHP1.2から導入されたPaginationですが、実態はfind('all')+ページ情報をセットする処理を隠蔽したものとなっています。ポイントは、$controller->paginate()メソッドの中で、これらの処理を全部行いつつも、returnで戻ってくるのはfind('all')の結果なので、paginate()の結果だけキャッシュさせていても、ページ情報がキャッシュされずにView側でエラーになる、ということです。(このあたりのページ処理についてはcake/libs /controller/controller.phpの1056行目あたりにあります) この理由から、controller側でpaginateの結果をキャッシュさせる作戦はうまくいかないので、Paginationは毎回遅くなります。
そこで、controller側ではなく、Model側でキャッシュを行わせます。paginate()内で行われるfind以外の処理、つまりページ情報の処理についてはその処理時間は高々知れているので、結局findの処理をキャッシュさせておくことでpaginate()の高速化が期待できます。
導入方法
今回は、モデルキャッシュをmemcachedにキャッシュさせるようにしました。コードは上記リンクから入手できるので、app/models/behaviorにcache.phpで保存しておきます。
まず、app/config/core.phpのCache::configを次のようにFileエンジンの設定をコメントアウトし、Memcacheエンジンを利用します。
Cache::config('default', array(
'engine' => 'Memcache', //[required]
'duration'=> 3600, //[optional]
'probability'=> 100, //[optional]
'prefix' => Inflector::slug(APP_DIR) . '_', //[optional] prefix every cache file with this string
'servers' => array(
'127.0.0.1:11211' // localhost, default port 11211
), //[optional]
'compress' => false, // [optional] compress data in Memcache (slower, but uses less memory)
));
//Cache::config('default', array('engine' => 'File'));
そして、キャッシュを利用したモデルにおいてactsAsで設定します。
var $actsAs = array('Cache');
findをオーバーライドします。僕は、findの第一引数にいろんなバリエーションを持たせることをよくやるので、
function find($type, $queryData = array()){
switch ($type) {
case 'popular' :
return $this->find('all', Set::merge(array("conditions"=>array("Model.rating"=>5)), $queryData));
case 'latest' :
return $this->find('all', Set::merge(array("limit"=>10, "order"=>"Model.created"), $queryData));
default:
$args = array($type, $queryData);
if ($this->Behaviors->attached('Cache')) {
if($this->cacheEnabled()) {
return $this->cacheMethod($cache_time, __FUNCTION__, $args);
}
}
$parent = get_parent_class($this);
return call_user_func_array(array($parent, __FUNCTION__), $args);
}
}
こんなかんじで、第一引数のタイプをcase文で振って、return文では素のfind('all')を通らせることで必ずdefaultに処理を落とすようにさせ、そこでキャッシュをかけるようにさせます。これでシンプルでスッキリした構成にできました。
まとめ
1つ思ったのは、このビヘイビアではcacheMethodメソッドにおいて第一引数ではキャッシュ時間を指定させるのですが、この時間は第三引数にしてオプション扱いにしてほしかったかなぁということ。と、いうのは単純な話で、このメソッドにおいて重要度は __FUNCTION__ > $args > $cache_timeであるから。組み込みのエンジンを利用できるのだからcacheのexpireもCache::configのduration値を標準で見てくれてもいいのかな、と思います。
とはいえ、これでコントローラ側では普通にfindを呼び出すだけでキャッシュ付きの処理を行うことができるので、非常に有効なライブラリかと思います。ちなみにキャッシュはModelに対応したテーブルのレコードが変化するたびにクリアされる(=afterSaveをフックしている)ので、頻繁に saveが起こるようなModelでは、あまりキャッシュが有効にならない場合も多いかと思うので、その点はご注意を。
yumで入れたPHPをソースからコンパイルしたPHPと入れ替える
Apacheが最近segmentation faultでコケることが数回あったので、原因を探るためにdebugを有効にしたPHPに入れなおすことにしてみました。OSはLinux(Fedora9)で、Apache, mysqldはyumで入れていたものをそのまま使うことを想定しています。以下、作業メモです。
既存のPHPを削除
もともとyumで入っていたので、そのまま素直にyumで削除します。
sudo yum -y remove php\*
ソースコードの入手
PHPの最新のソースコードを入手します。ソースコードはここから入手できます。2009.01.06時点での最新バージョンは5.2.8です。
Configureの準備
Configureにあたって、多くのツールをインストールしておく必要があります。僕は次のものを入れる必要がありました。(場合によってはまだほかにも必要かも?)
sudo yum -y install ncurses\* sudo yum -y install gcc-c++ sudo yum -y install flex sudo yum -y install libxml\* sudo yum -y install gdbm-devel sudo yum -y install gd gd-devel freetype freetype libpng libmng\* libtiff\* libjpeg\* libc-client\* giflib\* sudo yum -y install httpd\* sudo yum -y install pcre-devel sudo yum -y install unixODBC-devel sudo yum -y install net-snmp-devel sudo yum -y install openssl\* sudo yum -y install bzip2\* sudo yum -y install curl\* sudo yum -y install gdbm\* sudo yum -y install db4\* sudo yum -y install gmp\* sudo yum -y install libc-client\* sudo yum -y install openldap\* sudo yum -y install libmcrypt\* sudo yum -y install mhash\* sudo yum -y install freetds\* sudo yum -y install mysql\* sudo yum -y install postgresql\* sudo yum -y install aspell\* sudo yum -y install readline\* sudo yum -y install libtidy\* sudo yum -y install libxslt\* sudo yum -y install libtool\*
configure
yumで入れていたころのPHPのconfigure結果をできるだけ再現させてます。そこからサポートされていないオプションは外して、mysqlのprefixを変更、apxs2のオプションを追加させています。(yumで入れてたときのPHPはapxs2が指定されていなかったけど、なんで指定されてなかったのかよくわかんないです><)
./configure --build=i386-redhat-linux-gnu --host=i386-redhat-linux-gnu --target=i386-redhat-linux-gnu --program-prefix= --prefix=/usr --exec-prefix=/usr --bindir=/usr/bin --sbindir=/usr/sbin --sysconfdir=/etc --datadir=/usr/share --includedir=/usr/include --libdir=/usr/lib --libexecdir=/usr/libexec --localstatedir=/var --sharedstatedir=/usr/com --mandir=/usr/share/man --infodir=/usr/share/info --cache-file=../config.cache --with-libdir=lib --with-config-file-path=/etc --with-config-file-scan-dir=/etc/php.d --enable-debug --with-pic --disable-rpath --with-pear=/usr/share/pear --with-bz2 --with-curl --with-exec-dir=/usr/bin --with-freetype-dir=/usr --with-png-dir=/usr --enable-gd-native-ttf --without-gdbm --with-gettext --with-gmp --with-iconv --with-jpeg-dir=/usr --with-openssl --with-pcre-regex=/usr --with-zlib --with-layout=GNU --enable-exif --enable-ftp --enable-magic-quotes --enable-sockets --enable-sysvsem --enable-sysvshm --enable-sysvmsg --enable-wddx --with-kerberos --enable-ucd-snmp-hack --with-unixODBC=shared,/usr --enable-shmop --enable-calendar --without-mime-magic --without-sqlite --with-libxml-dir=/usr --enable-force-cgi-redirect --enable-pcntl --with-imap=shared --with-imap-ssl --enable-mbstring=shared --enable-mbregex --with-ncurses=shared --with-gd=shared --enable-bcmath=shared --enable-dba=shared --with-db4=/usr --with-xmlrpc=shared --with-ldap=shared --with-ldap-sasl --with-mysql=/var/lib/mysql --with-mysql-sock=/var/lib/mysql/mysql.sock --enable-dom=shared --with-pgsql=shared --with-snmp=shared,/usr --enable-soap=shared --with-xsl=shared,/usr --enable-xmlreader=shared --enable-xmlwriter=shared --enable-fastcgi --enable-pdo=shared --with-pdo-odbc=shared,unixODBC,/usr --with-pdo-pgsql=shared,/usr --with-pdo-sqlite=shared,/usr --enable-json=shared --enable-zip=shared --with-readline --enable-dbase=shared --with-pspell=shared --with-mcrypt=shared,/usr --with-mhash=shared,/usr --with-tidy=shared,/usr --with-mssql=shared,/usr --with-apxs2=/usr/sbin/apxs
その後、make, make install でPHPがインストールされます。
また、configure, makeあたりでmysql周りが原因でコケるときは次を実行してから、./configure からやりなおしたらうまくいくかもしれません。
export LDFLAGS="-L/usr/lib/mysql"
php.ini
makeするとカレントディレクトリにphp.ini-recommendedができているので、これを/etcにコピーして必要な項目を修正します。
cp php.ini-recommended php.ini sudo mv php.ini /etc/ (好きなように編集)
php.confの作成
make installするとhttod.conf(/etc/httpd/conf/httpd.conf)に次の1行が追加されています。
LoadModule php5_module modules/libphp5.so
ただ、できるだけhttpd.confは素のままにさせておいて追加項目は書きたくない主義なので、さっきの1行は削除してしまいます。かわりに/etc/httpd/conf.d/php.confを次の内容で作成します。
# # PHP is an HTML-embedded scripting language which attempts to make it # easy for developers to write dynamically generated webpages. # LoadModule php5_module modules/libphp5.so # # Cause the PHP interpreter to handle files with a .php extension. # AddHandler php5-script .php AddType text/html .php # # Add index.php to the list of files that will be served as directory # indexes. # DirectoryIndex index.php # # Uncomment the following line to allow PHP to pretty-print .phps # files as PHP source code: # #AddType application/x-httpd-php-source .phps
これでhttpdを再起動すると--enable-debugなPHPで再起動します。たとえば素のCakePHPなんかはこれで動作させることができます。
まとめ
なかなかconfigureを通すことができなかったり、mysqlとの連携がうまくいかなかったりしたのですがなんとかビルドすることができました。ずっとyumにお世話になっているとゼロからbuildするのもすごく苦労しますね。。。まーでもいい勉強になりました。
CakePHPで国際化対応するときは検索エンジンのクローラに気を付ける
Cakeではgettextを利用して多言語化(国際化対応)が簡単にできる仕掛けが用意されています。
- __()関数を利用して文字列生成
- cakeコンソールでpotファイルを作成
- poエディタなどで各言語別に翻訳
実際は、こんな流れになります。詳しくは次のサイトなどが非常に詳しい情報が掲載されています。
- CakePHP1.2の簡単国際化 - CakePHP のおいしい食べ方
- CakePHPで国際化の方法を試してみましたCommentsAdd Star - アシアルブログ
- 1.2系の多言語対応メモ(1) - Writing Some Code
さて、この国際化対応のときに盲点となるのが「検索エンジン対応」です。
そもそものこの言語の切り替えというのは、HTTP RequestのAccept Languageを調べてそこで切り替えが行われています。(ブラウザでの優先する言語で設定できるやつですね。)ところが、検索エンジンのクローラのように、Accept Languageが設定されていないようなHTTP Requestが投げられると、意図しないページを収集されてしまうことがあります。
たとえば国際化対応しつつも、メインのターゲットを日本人に絞ったようなサイトを用意したとき、クローラはAccept Languageが設定されてない(ケースが多い)ので、上記サイトのような構造にした場合、標準言語である英語サイトの情報がクローラに収集されてしまいます。その場合、当然「海外サイト」と見なされ、いくらmeta要素でキーワード設定をしていても検索結果になかなか引っかからずにSEO的に残念な結果になってしまう、なんてことになりがちです。
クローラ固有の対策をする
じゃぁどうすればいいか、となると話は単純でクローラのUAを見て利用する言語情報を切り替えればOKです。CakeだとAppControllerにこんなコードを入れておくとよさげ。
$user_agent = strtoupper($_SERVER["HTTP_USER_AGENT"]);
if(strpos($user_agent, 'GOOGLEBOT')){
Configure::write('Config.language',"ja");
}
GOOGLEBOTは当然GoogleのクローラのUAです。他のエンジンにも対応したい場合は、同じようにUAを調べて条件分岐に追加すればOKです。また、言語切り替えの方法はConfig.languageを設定することになります。
よく問題になるのが、検索エンジンがクロールした内容と大きく変わる内容を、サイトアクセス時に返して検索結果を汚す、なんて話がありますが、今回のように表示言語を切り替えるくらいだと問題にはならないはずです。(また、実際に同じようなコードを入れ込んでいますが、問題にはなっていないです)
それにしてもクローラもAccept Languageをリクエスト時に投げてくれてもいいのに、、と思うのですが、リクエストヘッダはできるだけ軽く、というものがあるんでしょうかね?
CakePHPのアソシエーションでInner Joinを利用してレスポンス速度を向上
レコードサイズが大きくなってくるとhasOneやbelongsToのアソシエーションでかなり時間を食うときがあります。特に大きな処理をしなくても、ページアクセス時にControllerでdescribe <Table>して、結合した結果を舐めて時間が食われます。
いくらなんでも時間かかりすぎだろ、、と思ってよく調べてみたらCakeでのテーブル間JoinてLeft Joinになってるんですね。クエリ凝視するまで気づかなかった。これ、特に問題なければ内部結合(Inner Join)にするだけでレスポンス速度は大きく変わります。方法はModelでアソシエーション対象Model名のtypeを"INNER"にするだけ。
<?php
class User extends AppModel {
var $name = 'User';
var $hasOne = array(
'Profile' => array(
'className' => 'Profile',
'foreignKey' => 'user_id',
'conditions' => '',
'type' => ' INNER'
)
);
}
?>
こういうテクはもちろんケースバイケースですけど、意外に盲点なチューニング方法かもしれません。
MecabとLingua::JA::Summarizeで文章のキーワード抽出をCakePHPで
文章中のキーワード抽出を行いたくなっていろいろ調べていて、次の組み合わせで実現することができました。
- Mecab
- Lingua::JA::Summarize
- Pecl/Perl
Mecabは文書の形態素解析に。Lingua::JA::Summarizeはサイボウズラボ奥さんのキーワード抽出CPANモジュール。これをCakePHPに組み込みたかったのでPeclのPerlライブラリ(PHPからPerlのコードをダイレクトに呼べる)。導入も特に難しくないので、その導入メモを残しておきます。
Mecab
Fedora系Linuxだとyumで辞書ファイルも一緒にさっくりインストールできます。Perlのモジュールも入れておきます。
sudo yum -y install mecab\* sudo yum -y install perl-mecab\*
Lingua::JA::Summarize
CPANからインストールできるのですが、僕の環境だとテストでエラーになるのでforce installします。あと、実行時エラーも出たので、次のようにインストールすると回避できました。あらかじめ
alias cpan='perl -MCPAN -e shell'
と、しています。
sudo cpan install HTML::Strip install Jcode install Class::ErrorHandler force install Lingua::JA::Summarize
Pecl/Perl
これもpeclコマンドでOKです。あらかじめ次のものをインストールしておいたほうがいいかも。2行目の「sudo yum -y install perl-\*」はちょっと強引すぎる気が自分でもしてるのですが、何かしらのperl系モジュールがないとpecl/perlのインストールにコケてしまったことは確か。(このあたりの詳細のメモを失念。。。)
sudo yum -y install php-devel sudo yum -y install perl-\*
この上で
sudo pecl install pecl/perl
また、extensionにperl.soを登録します。
sudo vi /etc/php.d/perl.ini
で、次の1行を追加。
extension=perl.so
これで、Apacheを再起動するとPHPでキーワード抽出が可能になります。
CakePHPに設置
app/vendors にLingua/JA/ディレクトリを作成し、そこにSummarize.phpを作成します。
Summarize.php
<?php
class Summarize{
private $summarize;
public function __construct(){
$perl = new Perl();
$perl->eval('use HTML::Strip;');
$perl->eval('use Lingua::JA::Summarize;');
$this->summarize = new Perl ("Lingua::JA::Summarize", "new",
array(
"charset" => 'utf8',
"mecab_charset" => 'utf8',
"default_cost" => 2.5,
"singlechar_factor" => 0.2
)
);
}
function getKeyword($key, $maxNum=15){
$this->summarize->analyze($key);
return $this->summarize->array->keywords(
array(
"threshold" => 5,
"minwords" => 1,
"maxwords" => $maxNum
)
);
}
}
?>
これをapp/config/bootsrrap.phpで読み込ませます。次の1行を最後に追加。
App::import( 'Vendor', 'Summarize' , array('file'=>'Lingua' . DS . 'JA' . DS . 'Summarize.php'));
これでキーワード抽出用のモジュールSummarizeが読み込まれるので、たとえばControllerで次のような処理を行うことができます。
$summarize = new Summarize(); $keys = $summarize->getKeyword($word, $num);
$wordは抽出対象の文章、$numは抽出キーワード数です。$keysは抽出されたキーワードの配列が返ります。返ってくるキーワードの抽出に違和感があれば、Summarize.phpのdefault_cost、singlechar_factorあたりをチューニングしてみましょう。このあたりのパラメータについては奥さんのドキュメントが最も分かりやすくまとまっていると思います。
CakePHPでBasic認証対応ページを作る
1.2だとBasic認証対応ページを作るのも超簡単です。
対応させたいControllerにSecurityコンポーネントを適用
まずSecurityコンポーネントを利用します。
class HogeController extends AppController {
var $name = 'Hoge';
var $uses = array('Hoge');
var $components = array('Security');
...
}
認証情報を追加
beforeFilterに認証に必要な情報を追加します。
function beforeFilter(){
parent :: beforeFilter();
$this->Security->loginOptions = array('type'=>'basic');
$this->Security->loginUsers = array('katsuma'=>'katsukatsu');
$this->Security->requireLogin('*');
}
ここではUser:katsuma, Pass:katsukatsuの認証情報で、全アクションで認証を必要とさせています。認証を特定のアクションのみに限定させたいときは、requireLoginメソッドで、認証を実行するアクション名のリストを指定します。基本的には必要なことはこれだけです。簡単!
Formは要注意
一点、要注意な事項としてFormを必要とするView、たとえば何らかのPostを実行するようなFormのViewがあるときに、そのFormを構成する要素はすべてFormヘルパを利用する必要があるということです。
inputタグなんかの要素はもちろん、たとえばformタグを書き出すときも
<form action="/Hoge/add"
とかじゃダメで、
<?php e($form->create('Hoge', array('action'=>'add')))?>
じゃなければダメです。(こうしないと認証がかからない)
これはどうもヘルパを利用すると、Tokenのようなものを全要素に対して自動的に追加して、このTokenがちゃんと追加されていないとSecurityコンポーネントが働かないようです。「formタグくらいどうでもいいだろー」とタカをくくってて、痛い目にあったので皆さんもご注意ください。。
CakePHP勉強会に参加してきました
少し遅れましたが、先週末にトライコーンさんで開かれたCakePHP勉強会に参加してきました。PHPな方々のコミュニティに参加するのは初めて。JSerと比べると割とおとなしめ?でも、勉強会が定員オーバーになるまでの時間が回を重ねるごとにものすごい勢いで短くなっているので、皆さん静かな中に熱いものがある様子。以下、簡単なレポートです。
~フェイス女学園~ CakePHP を使った効率的なPC・モバイルサイト構築について
画像認識エンジンを組み合わせたWebサービス「フェイス女学院」ができるまでの話。携帯サイトの構築ネタは今後かなり盛り上がってきそうな気がします。
そんな中、フェイス女学院はlayoutでキャリアごとのHTMLを切り分ける、というテクで乗り切ったとか。以前に自分が携帯サイト作ったときはCakeのようなフレームワークは使わずにSmartyでオレオレフレームワークを作って乗り切ったのを思い出しました。画像認識エンジンのOpenCVとやらも気になりました。これか!
あと、TVの影響はかなり大きいんだな、 と再確認。TVてやっぱまだまだ一番のメディアですね。
CakePHPでの失敗談
バッドノウハウというか、世間では笑い話(常識?)なのかもしれないけども、普通に初めて知ったこともかなりありました。bindModel, startup(), Setクラス,な下りはよく分かっていないので復習。
ホッテントリメーカー@CakePHP(仮)
まわりがCakeCake言ってるので流れに乗ったそうな。そんな僕も同じクチです。
最初は勉強会に行っても全然分からなかったけど、そのうちに使えるようになったとか。もう少しサービスに特化した苦労話やら、工夫点なんかを聞かせていただけたらなぁとも思いました。
CakePHPゆとり開発環境
「stableがstableじゃない」に笑った。確かにそうなのかも。
Editor周りの話でEmacsか、Vimか?な話になってVImが一番多かった。秀丸派はぜんぜんいないのが意外。僕は普段LLはもっぱら秀丸なんですけどね。Ustreamのチャットでakiyanさんが「僕、秀丸派ですよ!」みたいな主張をされてたのがちょっとツボに。
AuthComponentをOpenID対応
CakeでOpenID導入。OpenIDの統合は前からかなり興味ある話だったので結構惹かれる話でした。LTでかなりまとめて話されてたかと思うのですが、結構簡単に導入できそうな感じ。ソース公開されているので、あとでチェック。
CakeでTest
ちゃんとしたテストコードはなかなか書けていないので、普通に役立ちました。SimpleTestを使えばよさそう。あと本家のテストコードを読んだ方がうやむやにされてるAPIとか関数の使い方をうまく理解できそうです。実際1.2のvendor周りの仕様がよくわかんなくて、このあたりはテストコードで理解できました。
CakePHPとsymfony
symfonyの公式アプリ?のaskeetをCakeに移植する話。Cakeでは複合PKが使えないけど、Symfonyはそうじゃない。Cakeではクラスごとにいろんなメソッドが用意されているのに比べてSymfonyではグローバルな関数がいろいろ用意されてある、Cakeは配列ベースでSymfonyはオブジェクトベース、Cakeは細かいクエリを投げまくってSymfonyはまじめにJOINする、など、Symfonyの話はまったく知らなかったので、比較対象としていい話でした。あと単純な速度面での性能比較はCake1.1 > Symfony > Cake1.2みたい。この辺り、速度向上ネタも、今後は聞いてみたいですね。
まとめ
Shibuya.JSなんかは「なんですかそれw」みたいなネタが結構多いのですが、Cake勉強会では「あるある」と思えるネタも多くて分かりやすい話も多かったのが印象的でした。全般的に「どう使うか?」と「どう使いこなすか?」のネタの割合が半々くらいだった気がしたので、今度は後者の話をもっと聞きたいな、とも思います。特にパフォーマンス関連のネタとか。(他力本願)
あと、CakePHPのディベロッパのgwooさんも次回は参加したいとの表明をされているようで、そのあたりの動向も気になるところです。質の高いインプットが多かったので次回もぜひ参加してみたいですね。
CakePHPでランダムにレコードを取得する独自findメソッド
CakePHPも1.2になって、findAllが非推奨になってfind('All')に置き換わったように、findHoge系なメソッドは全部findに集約して、第一引数でそのselectタイプを指定するように仕様が変更になりました。最初はこの流れはちょっと面倒くさいなぁとも思ったのですが、実際は自分の都合のいいようなタイプをどんどん追加しやすくなっているので、この仕掛けはうまく使えばすごく便利。
たとえば、タイトルのようなもの。あるUserテーブルの中からランダムに50人分のレコードを取り出すfind('random')とか実装したいときはこんな感じ。
<?php
class User extends AppModel {
var $name = 'User';
function find($type, $queryData = array()){
switch ($type) {
case 'random':
return $this->find('all',
array(
'order' => 'rand()',
'limit' => 50
)
);
default:
return parent::find($type, $queryData);
}
}
}
?>
$typeで追加したいタイプを指定してswitchで振り分け、defaultのタイプについては親クラスのfindにそのまま処理を丸投げすることで、シンプルな機能拡張ができます。
あと、MySQLだと"order by rand()"でランダムに並び替えができるので、これを利用して先頭50件を取得しています。単純にランダムにするだけだとlimitはもちろんなしでOK。これ見たら分かると思いますけど、すごく簡単に機能拡張できるのがいいかんじ。最新20件を取得する「find('latest')」や、人気の20件を取得する「find('popular')」みたいなメソッドとか作っておくと、使い回し効きそう。
ところで
上で例で挙げたレコードのランダム取得とか、ついfind('All')でとりあえず全件取得してからcontrollerでごちゃごちゃした処理をかいたり、find('All')の第二引数で技巧的なオプションをつけてみたりしがちだけども、あらかじめメソッドを追加しておいたModelに丸投げしておくとcontrollerがすっきりシンプルになって、結果的に可読性もあがってよさげ。自分もfindの後でごちゃごちゃした処理をよく書いていたので、これを機にModelに処理を任せるようにいろいろリファクタリングしようと思います。
このあたりの話は、CakePHP開発者メンバーによる「Best Practices in MVC Design with CakePHP (php|architect’s C7Y)」がすごくいい話で、さらにSooeyさんがこれを和訳してくださってるのが素晴らしくいい感じ。Cakeに限らずMVCモデルなフレームワークの一般的な話としても通じる話だとお思うので、このあたりに携わる人とすれば一度目を通しておいて損はないと思います。
CakePHP1.2のエラーページは仕様が変わってる
(追記:2008/06/20)この内容は表現に不十分な点があります。文末に情報を追記しています。
CakePHPではControllerがみつからない、Viewが無い、なんかのエラーページは、CakePHP1.1ではapp/views/errorに
- missing_action.thtml
- missing_controller.thtml
- missing_view.thtml
なんかを用意しておけばカスタムエラーページを表示することができて、露骨に「loginのviewがありません><」みたいなエラーが表示されてセキュリティホールになりうることを回避できます。このviewの命名規則はCakePHP1.2も同じで
- missing_action.ctp
- missing_controller.ctp
- missing_view.ctp
を用意しておけばOKです。
共通処理についての仕様変更
さて、これらのviewに共通の変数を利用する場合、たとえばログインしているときのログイン名をヘッダに表示するなんてことをエラーページにも適用したい、という場面は当然あると思います。その場合、app/app_controller.phpを用意しておいて、beforeFilterで変数をセットしておけばありとあらゆるviewで適用できます。たとえばこんな形にしておけば全viewで$my_nameで名前の表示が行えます。
app/app_controller.php
<?php
class AppController extends Controller{
function beforeFilter(){
$my_name = 何らかの方法で名前を取得;
$this -> set('my_name',$my_name);
}
}
?>
ところが、1.2ではこの方法ではエラーページにかぎって名前が取得できません。他のAppControllerを継承したコントローラのページでは取得できるので、どうやらエラーページではAppControllerを継承していないor処理がスルーされている様子。
と、いうわけでエラーページの仕様が変わっているようなので、いろいろと調べてみるとエラーページはcake/libs/error.phpをコピーしてapp/error.phpに配置してあげると、エラー発生時に独自のハンドリングが行える模様。ファイルをコピーしてからapp/error.phpを開いてみるとこんな感じの記述が。
function __construct($method, $messages) {
App::import('Controller', 'App');
App::import('Core', 'Sanitize');
$this->controller =& new AppController();
$this->controller->_set(Router::getPaths());
$this->controller->params = Router::getParams();
$this->controller->constructClasses();
$this->controller->Component->initialize($this->controller);
$this->controller->_set(array('cacheAction' => false, 'viewPath' => 'errors'));
...
そう、app_controllerをimportして、そのコンストラクタを作成しているような様子です。にも関わらずbeforeFilterが適用されていないようなので、強制的に呼び出してあげればよさそうです。上のコードに次の1行を付け加えます。
function __construct($method, $messages) {
App::import('Controller', 'App');
App::import('Core', 'Sanitize');
$this->controller =& new AppController();
$this->controller->_set(Router::getPaths());
$this->controller->params = Router::getParams();
$this->controller->constructClasses();
$this->controller->Component->initialize($this->controller);
$this->controller->_set(array('cacheAction' => false, 'viewPath' => 'errors'));
$this->controller->beforeFilter();
...
これでエラーページでも名前を表示することができました。1.2のリリース版が出たら直ったりするんでしょうかね?取り急ぎ、エラーページで困った人はこれらの情報をもとに対処方法を考えてみてはいかがでしょうか?
CakePHPを1.1から1.2へ上げるときの注意点
(注意:まだ書きかけです→2008.06.18 書き終わりました)
2008.06.18現在、CakePHP1.2のrc版がリリースされてあり、1.1でアプリケーションを作っていた人もそろそろ1.2に上げようかな、、なんて思っているんじゃないかと思います。最近仕事でさくっとCakePHP1.1で作ったサイトがあったのですが、リリースが落ち着いた瞬間を狙って一気に1.2に上げてみました。そのときのメモを残しておきます。 今回は1.2.0.7125 RC1を利用しています。なお、この移行作業は「とりあえず警告が出ないレベルで正常動作する」ことを目的にして作業を行っています。なので、実際は非推奨の方法も混ざっていることもあるかと思いますが、ご注意ください。
cakeディレクトリ
ここは全部丸ごと入れ替えます。オリジナルのcakeディレクトリ丸ごと削除→1.2のcakeディレクトリをコピー。
viewファイルの拡張子を変更
1.1のときのviewのファイルの拡張子は.thtmlでしたが、1.2になって.ctpに変わっています。1ファイルづつ変更していても疲れるのでこんな感じで一気に変換します。
for file in *; do mv -i $file `echo $file | sed 's/.thtml/.ctp/'`; done
フォームヘルパの変更
フォームヘルパも書き方が変わって、
<?php echo $html->input('Timeline/title', ...); ?>
から、
<?php echo $form->input('Timeline.title', ...); ?>
に変わっています。ここで、とりあえず$htmlから$formにだけ変更しておきます。1.2.0.7125 RC1現在、$html変数を利用すると警告が出ますが、$html->inputの引数のフォーマットは1.1のままでも怒られないようです。これは秀丸のgrep置換などを利用して一気にやってしまいましょう。
app/config/routes.php
config系も記述の仕方が変更になっている点が多いです。まずroutes.phpはRouteオブジェクトがRouterクラスに変更になっています。元のapp/config/routes.phpを次のように変更します。
(1.1)$Route->connect('/home', array('controller' => 'pages', 'action' => 'home'));
(1.2)Router::connect('/home', array('controller' => 'pages', 'action' => 'home'));
app/config/core.php
core.phpはdefineで定義していたものがConfigureクラスを利用するようにします。元のapp/config/core.phpを次のように変更します。
(1.1) define('debug', 0);
(1.2) Configure::write('debug', 0);
ただし、基本的にはcore.phpに関しては1.2で内容がかなり変わっているので、実際は変更作業を行うよりも、1.2オリジナルのものを利用した方がよさそうです。
アプリケーション固有の定数
アプリケーション特有の定数宣言はどうするのがベストプラクティスなのか分かっていませんが、僕は1.1の時にはapp/config/app.phpと専用のファイルを作成して、defineで定義していたものを用意していたので、これをそのまま流用するためにbootstrap.phpからincludeさせています。
app/config/app.php
<?php
define('APPLICATION_NAME', 'HogeHoge');
define('APPLICATION_SERVER', '192.168.100.10');
...
?>
app/config/bootstrap.php
<?php ... // load const values require_once(APP . 'config/app.php'); ... ?>
このincludeはApp::importを利用する or Configure::writeで定義させる方法を利用した方がいいのかもしれません。(ご存知の方は教えていただければ幸いです)
Vendor系ライブラリ
サードパーティなライブラリはapp/vendorsに配置させてapp/config/bootstrap.phpからロードさせると思うのですが、ここでのロード方法も変更になっています。たとえば僕はdBugライブラリを利用しているので、これをapp/vendors/dBug/dBug.phpに配置しているのですが、このロード方法について、次のように変更になっています。
(1.1) vendor( 'dBug'.DS.'dBug' );
(1.2) App::import( 'Vendor', 'dBug', array('file'=>'dBug' . DS . 'dBug.php') );
どうも1.2になってキャメルケースの名前を発見すると自動的にアンダースコアをつけて解釈しちゃうようです。たとえば、
App::import('Vendor', 'SMTP');
と、書いた場合は
app/vendors/s_m_t_p.php
を見に行ってしまうようです(超厄介。。)サードパーティ製のライブラリについてはこのキャメルケースの解釈は勘弁してほしいんですけども、そうは言っていられないので、上でかいたほうに、arrayでフルパスを指定することでこの仕様を回避しています。
まとめ
手元の環境ではこんな感じの移行作業で動作しました。ある程度予想はしていたものの、やっぱり実際に作業してみると結構時間がかかりましたね。。特に定義系の表記法法の仕様変更を追いかけるのに苦労しました。
ただ、実際に移行作業をしてみると1.2の方が圧倒的に多機能で拡張性も豊かなので、移行のメリットは充分にあるかと思います。移行をためらっている方も、そろそろ「えいや!><」の勢いで移行を検討されてはどうでしょうか?
PHPでmemcachedを利用する
(2008.06.18 追記)タイトルのスペルが間違っていたので訂正しました。パーマリンク名も間違ってる。。けどこれは仕方なくこのままにしておきます。orz
最近memcachedを使うことがあったので、使えるようになるまでの個人的メモです。基本的にyum, peclコマンドだけでインストールは可能です。対象OSはFedora8です。
sudo yum -y install memcached sudo yum -y install php-devel #phpizeコマンドを利用するために必要 sudo yum -y install php-pecl\* sudo yum -y install zlib sudo pecl install memcahce
こんな感じでインストールはOKです。あとはmemcache.soをロードするように設定します。php.iniを直接編集してもいいのですが、extension系は/etc/php.d/に別途iniファイルを用意しておいた方が管理しやすいかもです。
sudo vi /etc/php.d/memcache.ini
で、
extension=memcache.so
の1行だけ書いたファイルを作成して、保存。
sudo /sbin/chkconfig memcached on
で、デーモンとして登録しておくと便利です。
ラッパクラス
peclの関数をそのまま利用してもいいのでしすが、ラッパ関数を作成してもいいかもです。僕はこんな感じのものを作成しました。MEMCACHED_SERVER_ADDR. MEMCACHED_SERVER_PORTは適当な値に置き換えてください。多分localhost, 11211番ポートになるはず。
<?php
class MemcacheManager {
private static $cache = null;
private function __construct(){}
static function getInstance(){
if(MemcacheManager::$cache == null){
MemcacheManager::$cache = new Memcache;
MemcacheManager::$cache -> connect(MEMCACHED_SERVER_ADDR, MEMCACHED_SERVER_PORT);
}
return MemcacheManager::$cache;
}
function get($key){
return MemcacheManager::$cache -> get($key);
}
function set($key, $var, $flag = null, $expire = CAKE_SESSION_TIMEOUT){
return MemcacheManager::$cache -> set($key, $var, $flag, $expire);
}
function close(){
return MemcacheManager::$cache -> close();
}
}
?>
そうした後で、こんな感じで使います。
$cache = MemcacheManager::getInstance();
$cache -> set('name', 'jkondo');
echo $cache -> get('name'); // 'jkondo'
このMemcacheManagerみたいなクラスを用意しておくと、memcachedが公式にはサポートされていないCakePHP1.1なんかでもvendorsディレクトリあたりに入れておくと、そのまま使えてしまうので結構便利。ご参考ください。(1.2になるとCacheクラスに統合されているようですね)
JSON用PECLパッケージのインストール方法
個人的なメモです。php 5.2.0以降は標準で入っているPECLのJSON モジュールのインストール方法。手元のテスト環境でphpのバージョンをupgradeさせるのが面倒そうだったのでPECLでインストールしました。そのメモです。(実はPECL使ったの初めて)
jsonモジュールのインストール
sudo yum -y install php-pecl\* sudo yum -y install php-devel sudo pecl install json
php.iniの編集
# コメントアウトされてあったらコメントを削除 extension_dir = "/usr/lib/php/modules" # 標準でロードする extension=json.so
上書きしたらhttpdを再起動するとOK。json_encode($array), json_decode($str)の関数が標準で使えるようになります。
サーバサイドでOS・ブラウザ判定
「そんなのできたらOS毎に分けたHTMLを出力するとき、無駄なdocument.write()だらけにならずに便利なのに、、、」と思ったのですが、よくよく考えたらUserAgent見たらできそう。と、いうわけでPHPで書いてみました。
<?php
$ua = $_SERVER['HTTP_USER_AGENT'];
if(eregi('Windows', $ua)){
echo('Windows!');
} else {
echo('Not Windows!');
}
?>
超簡単。UA送ってくれてたらブラウザ判定もいけます。
もちろん「UA偽装されたら、、」とかな話もあるけど、JSに頼らず"そこそこの信頼度"で知れることも大事かな、と。あと、サーバサイドで判定していたらSmartyなんかのテンプレートシステムでコード分けがものすごくシンプルになります。
{if $os=='win'}
<h1>For Windows User</h1>
{/if}
こんな感じでtplファイルに書けます。これをクライアントサイドでやると
if(os=='win'){
document.write('<h1>For Windows User</h1>');
}
こんな感じですかね。クライアントサイドでの判別方法の実装側の問題点として、ソース内のHTMLの実体が文字列ばっかりになっちゃって何だか分からなくなること多いこと。タグが入れ子になってきたらよくミスっちゃうことも多いはず。と、いうわけで、ずっとクライアントサイドに振るべきだと思われてた仕事も、サーバ側に振れる仕事は振っちゃってもいいのかな、と思いました。
PHP+Smartyで3G携帯サイトを効率的に構築する
最近、新規プロジェクト案件で携帯サイトの構築についていろいろ調査をしています。最初から携帯サイトの構築については、かなりいろいろな点で懸念はしていたのですが、蓋を開けてみると「やはり、、」と、いうかハマる点がかなり多いです。
そもそも、今回のプロジェクトにおいていろいろなサイトを調査していたのですが、まだまだPCサイト(XHTML+CSS+JavaScriptなサイト)に比べて、有益な情報がまとまっていないなぁ、、という感想です。 ウノウラボさんは本当に素晴らしい情報を開示してくださっていると思いましたが、かゆいところに手が届くような情報はまだまだ世の中に広がっていないようですので、僕が調べた点や、実装を進める上で得たTipsなどを共有していきたいと思います。そこで、今回はPHPで携帯サイトを実装する上でのTipsを記しておきたいと思います。
URLのような長い半角英数文字でレイアウトが崩れるのを防ぐ
Web屋泣かせの話として、ユーザの入力文字を表示する際に、ユーザがありえないような長い文字を入力してページレイアウトを崩れさせる、な話があるかと思います。全角文字の場合はCSSで表示ブロック要素に対してwidthをちゃんと定義しておくと崩れることはそうそう無いと思うのですが、問題は半角英数文字の場合。特にイジワルをしなくても長いパラメータが付いたURLなんかの場合は、意図せずともこんな場面に出くわすことになるかと思います。
そこで超便利なタグでwbrというタグがあります。これ、W3C の HTML/XHTML の仕様には無い拡張仕様なんですけども、そこが目をつぶれる場合ならかなり有効です。
PHPでffmpegの出力を格納する
Webアプリケーションにおいてffmpegを利用してメディアファイルをゴニョゴニョ操作する、なんて場面がここ最近増えてきていると思います。そんな中、ハマるポイントとして、ログの扱いがあると思います。
たとえば、あるファイルfooを違うフォーマットのファイルbarに変換するとき
ffmpeg -i foo bar
これで変換できます。(Codecの指定なんかはここでは割愛します。)なので、これをPHPから実行する場合はこんな形になります。外部プログラムを実行するのでexec関数を利用します。
<?php
$in = 'foo';
$out = 'bar';
exec('ffmpeg -i ' . $in . ' ' . $out);
?>
では、ここで実行時のログを保存したい、とします。execは第二引数に標準出力の結果を配列で受け取ることができるので、普通に考えると次のようになります。
<?php
$in = 'foo';
$out = 'bar';
$log = '';
exec('ffmpeg -i ' . $in . ' ' . $out, $log);
// output log
var_dump($log);
?>
さて、どうでしょうか?実はこれでは正しい結果は出力されません。試してみると分かるのですが、実は上記のコードを実行すると、結果の冒頭2行分しか$logには格納されていません。おそらく次のような文字列が出力されたと思います。
ffmpeg version 0.4.9-pre1, build 4718, Copyright (c) 2000-2004 Fabrice Bellard built on Jul 12 2007 12:03:11, gcc: 3.2.3 20030502 (Red Hat Linux 3.2.3-56.fc5)
これはffmpegを利用した際に必ず出力されるログです。「いや、本当に俺が必要なのはその次からのコードなんだよ!!」と叫びたくなるのですが、なぜか微妙なところで出力は終わってしまいます。何とか全部の結果を格納できないのでしょうか?
Crypt_RSA_KeyPairでfromPEMStringが動かない
PHPで公開鍵暗号を利用する場合、PEARのCrypt_RSAが利用できます。
このCrypt_RSAで、KeyPairをPEM形式の文字列から生成するためにCrypt_RSA_KeyPair::fromPEMString($pem) な関数があるのですが、手元の環境では動きませんでした。原因はundefinedな配列を操作しているみたいで。と、いうわけで以下、修正方法。
