肉とビールとパンケーキ by @sotarok

少し大人になった「肉とご飯と甘いもの」

Behat でメソッド定義して正規表現にもマッチしているはずなのに「未定義」と言われてハマった件

原因が分かったときに死ぬほど悲しかったが、ひとまず記録しておく。

    ...
    When I run "git daily init"
    ...

とか書いてて、メソッドも

<?php

    // ...

    /*
     * @When /^I run "git daily(?: ([^"]*))?"$/
     *
     * @param   string  $args
     */
    public function iRunGitDaily($args = '') // {{{
    {
       // ...   
    }

と定義してあって、これで間違いなく正規表現にマッチしているはずなのに、

未定義のステップを、次のスニペットで実装できます:

    /**
     * @When /^I run "([^"]*)"$/
     */
    public function iRun($argument1)
    {
        throw new PendingException();
    }

などと言われ、やたらハマってたんだけど、落ちついてドキュメントをもう一度読んでいたら、

Notice the comment block starts with /**, and not the usual /*. This is important for Behat to be able to parse such comments as annotations!

Defining Reusable Actions - Step Definitions — Behat 2 documentation

。。。oh ... なるほど ...

メソッドの定義を

<?php

    // ...

    // ↓ここが ** じゃなくて * になってた
    /**
     * @When /^I run "git daily(?: ([^"]*))?"$/
     *
     * @param   string  $args
     */
    public function iRunGitDaily($args = '') // {{{
    {
       // ...   
    }

これで解決。
初歩的?

  • (追記) ソースまで追っかけてないけどこの仕様は多分 PHP の ReflectionMethod の getDocComment() メソッドの仕様によるものとおもわれる

pecl install hoge でインストールしたとき、インストール先が extension_dir じゃないディレクトリになってしまう場合

Debain での話。

  • 手元で色々 PHP いじってて環境が微妙なことになってしまったとき
  • まぁ、こういうことになる人ってあんまいないと思うけど
$ sudo pecl install hoge

でインストールすると、

...
checking for PHP includes... -I/usr/include/php -I/usr/include/php/main -I/usr/include/php/TSRM -I/usr/include/php/Zend -I/usr/include/php/ext -I/usr/include/php/ext/date/lib
checking for PHP extension directory... /usr/lib/php/20100525-debug
checking for PHP installed headers prefix... /usr/include/php
...

おや?
で、当然 /etc/php5/conf.d/http.ini とかに

extension=http.so

とか書いても、

$ php -v
PHP Warning:  PHP Startup: Unable to load dynamic library '/usr/lib/php5/20090626/http.so' - /usr/lib/php5/20090626/http.so: cannot open shared object file: No such file or directory in Unknown on line 0
...

とかいわれる。
extension のインストールされたディレクトリと、phpの設定がもってる extension dir が違うようだ。

$ php -i | grep extension_dir
extension_dir => /usr/lib/php5/20090626 => /usr/lib/php5/20090626

となるから当然で。

あれ、じゃあなんで pecl install するときにここに入らないのだ、と思い一応 pecl config-show で確認。

$ pecl config-show
...
PHP extension directory        ext_dir          /usr/lib/php5/20090626
...

ちゃんとなっている。

ビルドのときには /usr/bin/php-config が使われるのでそれも確認

$ which php-config
/usr/bin/php-config
$ php-config
...
  --libs              [-lcrypt   -lz -lcrypt -lonig -lcrypto -lssl -lcrypto -ldb-4.8 -lqdbm -lbz2 -lz -lcrypto -lssl -lcrypto -lrt -lm -ldl -lnsl  -lxml2 -lgssapi_krb5 -
lkrb5 -lk5crypto -lcom_err -lxml2 -lxml2 -lxml2 -lcrypt -lxml2 -lxml2 -lxml2 -lxml2 -lcrypt ]
  --extension-dir     [/usr/lib/php5/20090626-debug]
  --include-dir       [/usr/include/php5]
...

あれ。

php-config は誰によってインストールされるか、

$ dpkg -S php-config
php5-dev: /usr/share/man/man1/php-config5.1.gz
php5-dev: /usr/bin/php-config5

php5-dev。だけど /usr/bin/php-config5 ってことは多分 /usr/bin/php-config は symlink にしてるんだな、と確認してみると、

$ ls -la /usr/bin/php-config
-rwxr-xr-x 1 root root 4570 2012-02-03 08:36 /usr/bin/php-config

あれ。実体だ。

つまり原因は、

  • 何かしらのタイミングで php-config が実体になっちゃった
  • php5-dev をインストールしなおしても、update alternatives が働かない
  • 何かしらのタイミングで実体になっちゃったものがずっとつかわれちゃってる
  • pecl install のときにおかしくなる

ということなので、 /usr/bin/php-config を削除して php5-dev のインストールしなおし

$ sudo rm /usr/bin/php-config
$ sudo apt-get remove php5-dev
$ sudo apt-get install php5-dev

...

php5-dev (5.3.10-1~dotdeb.1) を設定しています ...
update-alternatives: /usr/bin/php-config (php-config) を提供するために 自動モード で /usr/bin/php-config5 を使います。
update-alternatives: /usr/bin/phpize (phpize) を提供するために 自動モード で /usr/bin/phpize5 を使います。m

で、再度確認

$ ls -la /usr/bin/php-config
lrwxrwxrwx 1 root root 28 2012-02-06 11:47 /usr/bin/php-config -> /etc/alternatives/php-confi

$ php-config
...
  --libs              [-lcrypt   -lz -lcrypt -lonig -lcrypto -lssl -lcrypto -ldb-4.8 -lqdbm -lbz2 -lz -lcrypto -lssl -lcrypto -lrt -lm -ldl -lnsl  -lxml2 -lgssapi_krb5 -
lkrb5 -lk5crypto -lcom_err -lxml2 -lxml2 -lxml2 -lcrypt -lxml2 -lxml2 -lxml2 -lxml2 -lcrypt ]
  --extension-dir     [/usr/lib/php5/20090626]
  --include-dir       [/usr/include/php5
...

OK。
で、 pecl install してめでたし。

追記

最初の php-config のときに

  --version           [5.4.0beta1-dev]
  --vernum            [50400]

が出てたので自分で作ってた php5.4 の .deb をインストールしたときだなー間違いなく。

ますますこの問題おこるひとほとんどいないきがするよ。。
起こるとしたら、debian標準のaptから dotdeb に切り替えたとか、何かしら環境を変えた時とか。

PHP Apocalypse で発表してきました #phpapoc

PHP - Be Happy with PHP というタイトルで発表してきました。PHPというか、なんとなく、PHP全般とか、チーム開発とか、開発全般とかの話です。



※スライドだけ見ても全然伝わらない風の発表でしたね

今後の話とか、その他のまとめ

  • 今回の発表は、今、僕が感じていることであって、今後僕はまたいろんな壁にあたったり乗り越えたり挫折したりしながらきっと成長していく(つもり)ですし、その時に何考えてるのかはわかりませんし、未来の僕がこのときの自分を振り返ってあの頃はガキだったな、と思うことは多分ありますし、まぁそうでないといけないですし、などなど色々あります。
  • なので、そういった意味でも、最初のお断り通り、この話をどう受け取るかは聞いてくださったみなさん次第です。まぁそもそもまとまってないですが。
  • @koriyam さんの話はとても共感できるものでした。僕の話まとまらなかったけどとってもまとまっていて素敵な発表でしたね。
  • 最後に、いろいろまとめてくれた @tsuyoshikawa、会場運営のカトーくん、会場貸してくださったグリーさん、ありがとうございました。

最近お気に入りのPHPライブラリ開発手法

PEAR2/Pyrus ってどうなったんだっけ?
という話はとりあえず一旦置いておいて、最近わりかしカジュアルにPHPライブラリを開発して配布する方法がなんとなく自分の中で定着してきたので超ざっくりまとめておく。

ソースコードはGitHub、開発にはgitflow、配布はOpenpear

Openpear で世界征服の話はどうなったんだ、というのは置いておいて、ざっくり、上記の通り、

というのが一番楽だと思っている。

ソースコードはGitHub

Openpear はとっても便利なサービスで、SVNでのホスティングもサポートしているのだけど、今更SVNを使いたくもないし、Openpear で今後Gitをサポートする予定もない。
GitHubではGitによるfork/pull request のエコシステムが出来上がりつつあると思っていて、開発者もそこにいるし、GitHubでなんか作っておけば人にコード見てもらったり修正してもらったり、それを取り込んだりが非常にやりやすい環境が揃っている。

もうGitHubを使わない手はない。

配布はOpenpear

で、GitHubで開発したものを直接PEARパッケージ化して、PEARコマンドでインストールできるようにしたいわけだ。
だけど、GitHub登場以来何度かForumに登場しているPEARの公式サポートの件はまとまらないまま話が立ち消えたりして、なんともなっていない。

と、ここで登場するのがOpenpearだ。

Openpear は、上記の通りSVNリポジトリホスティングはしているが、パッケージの設定で、外部のリポジトリSVN、Git、Mercurialが指定できる。

つまり、ここに、ホスティング先のGitHubのリポジトリを指定すれば、それだけでPEARパッケージ化することができるし、以下のコマンドでインストール可能な形式で配布できる。

$ sudo pear install openpera/Git_Daily


ところで、OpenpaerでGitHubからリリースするにはちょっとした設定のコツ?がいるというか、注意すべき点があるので、一応それはここで説明しておく。


参考画像:


以下参考:

開発ツールとしてgitflow を使う

gitflow は、上記Openpearの制約にぴったりなツールだ。
develop ブランチを開発用ブランチとして普段のやりとりにつかい、releaseブランチを経て、最終的にタグと共に master ブランチに merge される。master ブランチは、常に、安定してきた開発用のブランチからのマージコミットしか存在せず、つまり、master ブランチが、「配布して良いブランチ」となる。

参考:

あ、ちなみに、さっきから例に用いている git-daily というのは、gitflow に似たツールなんだけど、ライブラリ開発用というより、もっと頻繁に修正リリースとかが発生するようなウェブアプリ開発用のgit補助ツールであったりする。最近はいつも自分もコレを使ってウェブアプリの開発をしているわけだけど、これはこれで相当便利なのでまた今度話題にします。

一応参考:

具体的な手順

  1. 手元で、Gitでライブラリ開発を開始する
  2. git flow init して、develop で開発
  3. 適当なタイミングでGitHubにリポジトリを作ってpush
  4. developブランチでなんやかんやしてよしリリースやー、となったら git flow release start, -> … -> git flow release finish
  5. master にマージされるので、これをGitHubにpush
  6. Openpear にそのライブラリ用のパッケージを新規作成
  7. リポジトリ設定で、GitHubのURIを指定する
  8. リリース処理
  9. pear コマンドでインストール可能な形で配布完了

という感じです。あとはGitHubで開発継続、gitflowでmasterへマージ、Openpearで配布(リリース)を繰り返せばなんとなくうまくいく感じがしませんか。

まとめ

で、まぁ現状こんな感じ。
まとめると、繰り返しになるけど、

まぁ、PHPライブラリ開発の話ですが、そして個人的な話ではありますが参考になれば幸いです。


余談:

Openpear もっと海外でも使われれば色々良くなるよね、と思いつつ全然海外で広報活動をしていないんですけど。
あと、PEAR2を全然追ってないし、Openpear でそれをサポートするほど僕らの活力が残っていないので、いやむしろOpenpearってGitHubにあるから誰か(r

DotCloud で PHP アプリを設置してみたときの色々

beta の invite もらったので DotCloud で遊んでみました。

アプリ1つくらい設置してみないとなんだかよくわからないよねってことで、とりあえずどこで公開するかなーと思っていた、paste アプリ を設置してみた。


DotCloud の PHP の環境は、Ubuntu 上の nginx + php5-fpm (PHP 5.3.2) が標準で、なんというか時代の流れを感じました。いいね!
のは、まあいいとして、まぁ PHP アプリなんてものはたいてい Apache で動くことが前提とされてるもので (そうなのか? いや、そうだと思う。それが PHP のメリットだし) nginx な環境とか全然考慮してなかった + そんな PHP ユーザな弊害として nginx 力が低すぎてアレだったのですこしばかりはまったので、まあそのあたりのメモとして。

ということで、話は Pastit のディレクトリ構成を前提に進めますが、GitHub にあがってるので参考にしてください。

主な話の内容は、

  • DotCloud のアプリ設置操作
  • DotCloud の環境とか
  • DotCloud + PHP + nginx の環境とか

とりあえず設定

まあ Tutorial で言われてるとおりコマンドをインストールして、アプリケーションサーバMySQLサーバをつくってみる。
最初に dotcloud コマンドを使ったときに token をセットしろっていわれるけどこれはログインして setting を見たときに書いてあるやつな。

$ dotcloud list
$ dotcloud create pastit
$ dotcloud deploy --type php pastit.www
$ dotcloud deploy --type mysql pastit.mysql

MySQL の設定とかは以下で確認

$ dotcloud info pastit.mysql

cluster: wolverine
config:
    mysql_password: ****
created_at: 1304535584.4373901
name: pastit.mysql
namespace: pastit
ports:
-   name: mysql
    url: mysql://root:****@mysql.pastit.dotcloud.com:port
-   name: ssh
    url: ssh://dotcloud@mysql.pastit.dotcloud.com:port
state: running
type: mysql

MySQL は、以下でログインできるので、これでログインして DB 作ったりユーザ作ったりする。

$ dotcloud run pastit.mysql -- mysql -uroot -p\'****\'

で、ソースコードのデプロイ。今回は Git リポジトリです。Pastitのリポジトリのルートで実行。Gitのブランチを指定する場合は -b で。しなければ master が push される。

$ dotcloud push -b develop pastit.www .

で、http://www.pastit.dotcloud.com/ で動けばOK。今回は pastit.www という名前でデプロイしてるからこのURLになるけど、pastit.* に当たる部分が URL のサブドメイン *.pastit.dotcloud.com になる。

前提として知っておくべき DotColud の環境

push するとなにがどう設置されるか

DotCloud に push すると、以下のようなディレクトリ構成で設置される。とりあえず type = PHP の場合の話だが、アプリケーションサーバの場合他のタイプでも大差ないだろう。

dotcloud@pastit-www:~$ pwd
/home/dotcloud
dotcloud@pastit-www:~$ ls -la
-rw-r--r--  1 dotcloud www-data 54722 2011-05-06 18:53 build.log
drwxr-xr-x 13 dotcloud www-data  4096 2011-05-06 18:53 c1bda3a
lrwxrwxrwx  1 root     root         7 2011-05-06 18:53 code -> c1bda3a
lrwxrwxrwx  1 dotcloud www-data     8 2011-05-06 18:53 current -> code/www

この c1bda3a ってやつがコードの実態。
今回の場合リポジトリが Git だったので Git でのデプロイした時のコミットオブジェクトの hash 値になっている。
code ってやつは、常に最新のソースのルートへの symlink になっている。
current ってやつも symlink で、ここがアプリケーションの root (ドキュメントルート的な) の場所を指し示す。ここは、特に何も設定しなければ、 code と同じ場所を指すが、次に説明する dotcloud_build.yml で設定することで変更できる。

capistrano とかを使ったことがあれば、ソースコードが配置されて、最新のツリーへの symlink があって、みたいな構成はだいたい同じなのでキャッチアップしやすいと思う。

dotcloud_build.yml

DotCloud にアプリを push をすると、そのディレクトリをドキュメントルート *1 として設置される。多くのフレームワークのアプリの場合これは好ましくないはず。
なので、そのへんを設定してあげるのが docloud_build.yml。ここに、アプリの root がどこなのかという設定を記述できる (その他の設定項目もあるがとりあえずは省略) 。

このファイルは push するディレクトリのルートにおいておく。今回は approot として www ディレクトリを指定。

www:
  approot: www
nginx.conf と fastcgi.conf

approot 以下においておくと、読み込んでくれる nginx の設定。
といっても、Apache.htaccess とはちがって、単に nginx の設定で include されてるだけだから、pushしたときに nginx が reload されてはじめて設定は適用される。
両者の違いは、

  • nginx.conf : nginx のグローバルな設定
  • fastcgi.conf : location ~ /.+\.php$ に適用される設定

というだけ。これは設定が include される場所が違うだけ。PHP に関する設定は fastcgi.conf にしておくのがよさそう。


nginx.conf と fastcgi.conf は、deny 設定されてたのに、次に説明する postinstall だけ deny されてなかったので、それを nginx.conf に書いておいた。あと try_files の設定 (後述)。

try_files $uri $uri/ /index.php?$args;
location = /postinstall {
    deny    all;
}
postinstall

いかにも Debian 好きっぽそうなこの名前は、approot 以下に実行ファイルとしておいておくと、push の最後に実行してくれる。アプリケーションの設定の適用を行うファイルとかを適切においておけば良い。
ちなみに、Pastit は Ethna の config を home においておいて、それを push 時に適切なディレクトリにコピーするようにしている。PHP厨らしくちゃんとPHPで記述しておく。

#!/usr/bin/php
<?php

define('DOTCLOUD_HOME', '/home/dotcloud/');

$files = array(
    'pastit-ini.php'     => 'code/config/pastit-ini.php',
    'pastit-app-ini.php' => 'code/config/pastit-app-ini.php',
);

foreach ($files as $origin_file => $dest_file) {
    if (!file_exists(DOTCLOUD_HOME . $dest_file)
        && file_exists(DOTCLOUD_HOME . $origin_file))
    {
        echo DOTCLOUD_HOME . $origin_file, " -> ", DOTCLOUD_HOME . $dest_file, PHP_EOL;
        copy(DOTCLOUD_HOME . $origin_file, DOTCLOUD_HOME . $dest_file);
    }
}

// task
echo 'end of task', PHP_EOL;
その他

まあ ssh で入って /etc/nginx 以下をのぞけばわかることだけど、

nginx x PHP な設定

PAHT_INFO とか

PATH_INFO使ってる人ってどれだけいるのかわからないけど Ethna の UrlHandler はそれ使って path 解析するから、あるといい。
んだけど、PHP で一般的に知られている nginx 向けの PATH_INFO の設定だと、location が

/.+\.php.+$

にマッチするのが前提だったりしなくもないので、それだと、DotCloud 的にはよろしくない。というのは、DotCloud の PHP 向けの nginx の設定が

    location ~ /.+\.php$ {
        if ( -f /home/dotcloud/current/maintenance) {
            return 503;
        }

        try_files       $uri /static/404.html;

        fastcgi_pass    unix:/var/dotcloud/php5-fpm.sock;
        include         fastcgi_params;
        include         /home/dotcloud/current/*fastcgi.conf;
    }

となっているので、index.php/hoge とかにマッチしてくれない。

ならば /.+\.php.+$ にマッチする設定を approot/nginx.conf に書いておけば良いのかもしれないけど、fastcgi_pass のような DotCloud 環境依存な設定がそっちに記述されるのもどうかなと思うので、ここはゴリ押しで解決させておく。

index.php の先頭でこんなかんじで PATH_INFO のセットをしておいた

<?php
if (!isset($_SERVER['PATH_INFO'])) {
    $request_uri = isset($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : '';
    if (!empty($request_uri)) {
        $parsed_uri = parse_url($request_uri);
        $request_uri = isset($parsed_uri['path']) ? $parsed_uri['path'] : '';
    }
    $_SERVER['PATH_INFO'] = $request_uri;
}
rewrite とか

このあたりは DotCloud の Tutorial にあるように、try_files で設定しておくのがよさそう。$args も忘れずに。

try_files $uri $uri/ /index.php?$args;

よくある nginx.conf の設定だとここで

/index.php$requrest_uri

などをしておくことで PATH_INFO は設定するんだけど、そもそもこれは前述のとおり location でマッチできなくなるのでだめだった、というわけです。

その他PHPについて

PHP でも rake みたいなツールがほしい&一般的になってほしい
PHP だとどうしても設置後の task みたいなものを記述できる環境がないので、PaaS の恩恵でデプロイが楽になっても、アプリケーションのセットアップと設定管理みたいなものをどうするかって課題が残る。

今のところ、postinstall でなんかしら設定を行うように書いておくのがまあ普通かなーと思う。

まとめと雑感

  • DotCloud いいかんじ
  • サーバ建てるのとかデプロイとか超楽
    • PaaS的なものが増えてきてほんとアプリケーションエンジニアがインフラ触る機会って今後減ってくるんだろうなーと思った
  • Apache で簡単に動く PHP も好きだけど nginx な環境も最近は割と簡単に揃うし FPM とかも徐々に使われるようになるのかなーと思いつつ、色々な環境で動かせるようにしておくのは、たしかによさそう *2
    • まあいずれにしても Apache べったり、じゃなくてもいい時代かもね
  • DotCloud はその他ミドルウェアも充実してるからいろいろ遊びたいなー
  • あ、ってことで Pastit っていう、設置型ペーストアプリを公開しました。これはもとも Internal な環境で (社内とか) snippet の共有が簡単にできるといいなーと思って作ったので、使いたい方は設置してみるといいかもしれません、結構便利ですよ。まだ embed できないけど。

*1:nginx で Apache の DocumentRoot にあたるものってなんていうの?

*2:他の言語で FastCGIWSGI/PSGI などの動きがあるなか PHPFastCGI

PEAR で Unable to find the wrapper "channel" - did you forget to enable it when you configured PHP?

なんか出るよね最近。環境はDebian SqueezeのPHP 5.3.x。いや Lenny + dotdeb でも出る。(てか後述のとおり xdebug の設定だから環境依存じゃないかも。ちなみに PEAR は version 1.9.1)
検証してないけど channel:// を register してないのに使ってる気がする。

$ sudo pear upgrade-all

PHP Warning:  file_exists(): Unable to find the wrapper "channel" - did you forget to enable it when you configured PHP? in /usr/shar
e/php/PEAR/Downloader/Package.php on line 1517
PHP Stack trace:
PHP   1. {main}() /usr/share/php/pearcmd.php:0
PHP   2. PEAR_Command_Common->run($command = 'upgrade-all', $options = array (), $params = array ()) /usr/share/php/pearcmd.php:305
PHP   3. PEAR_Command_Install->doUpgradeAll($command = 'upgrade-all', $options = array (), $params = array ()) /usr/share/php/PEAR/Command/Common.php:271
PHP   4. PEAR_Command_Install->doInstall($command = 'upgrade-all', $options = array (), $params = array (0 => 'channel://components.ez.no/base', 1 => 'channel://components.ez.no/consoletools', 2 => 'channel://openpear.org/phpman', 3 => 'channel://pear.ethna.jp/smarty', 4 => 'channel://pear.ethna.jp/simpletest', 5 => 'channel://pear.ethna.jp/ethna', 6 => 'channel://pear.php.net/structures_graph', 7 => 'channel://pear.php.net/console_getopt', 8 => 'channel://pear.php.net/archive_tar', 9 => 'channel://pear.php.net/pear', 10 => 'channel://pear.php.net/xml_util', 11 => 'channel://pear.php.net/db', 12 => 'channel://pear.phpunit.de/dbunit', 13 => 'channel://pear.phpunit.de/file_iterator', 14 => 'channel://pear.phpunit.de/php_codecoverage', 15 => 'channel://pear.phpunit.de/phpunit', 16 => 'channel://pear.phpunit.de/phpunit_selenium', 17 => 'channel://pear.phpunit.de/php_timer', 18 => 'channel://pear.phpunit.de/text_template', 19 => 'channel://pear.phpunit.de/php_tokenstream', 20 => 'channel://pear.phpunit.de/phpunit_mockobject', 21 => 'channel://pear.symfony-project.com/yaml', 22 => 'channel://pear.twig-project.org/twig', 23 => 'channel://pecl.php.net/memcached', 24 => 'channel://pecl.php.net/xdebug')) /usr/share/php/PEAR/Command/Install.php:900
PHP   5. PEAR_Downloader->download($params = array (0 => 'channel://pear.php.net/structures_graph', 1 => 'channel://pear.php.net/cons
ole_getopt', 2 => 'channel://pear.phpunit.de/php_codecoverage', 3 => 'channel://pear.phpunit.de/phpunit', 4 => 'channel://pear.phpunit.de/phpunit_selenium', 5 => 'channel://pear.phpunit.de/phpunit_mockobject', 6 => 'channel://pear.twig-project.org/twig')) /usr/share/php/PEAR/Command/Install.php:661
PHP   6. PEAR_Downloader_Package->initialize($param = 'channel://pear.twig-project.org/twig') /usr/share/php/PEAR/Downloader.php:278
PHP   7. PEAR_Downloader_Package->_fromFile($param = 'channel://pear.twig-project.org/twig') /usr/share/php/PEAR/Downloader/Package.php:160
PHP   8. file_exists('channel://pear.twig-project.org/twig') /usr/share/php/PEAR/Downloader/Package.php:1517

.. 延々と

upgrade-all ok: channel://pear.php.net/Structures_Graph-1.0.4
upgrade-all ok: channel://pear.php.net/Console_Getopt-1.3.0
upgrade-all ok: channel://pear.phpunit.de/PHP_CodeCoverage-1.0.3
upgrade-all ok: channel://pear.phpunit.de/PHPUnit_Selenium-1.0.2
upgrade-all ok: channel://pear.phpunit.de/PHPUnit_MockObject-1.0.4
upgrade-all ok: channel://pear.twig-project.org/Twig-1.0.0RC1
upgrade-all ok: channel://pear.phpunit.de/PHPUnit-3.5.8

Warning: unlink(/tmp/glibctestQDzKXJ): No such file or directory in System.php on line 206
PHP Warning:  unlink(/tmp/glibctestQDzKXJ): No such file or directory in /usr/share/php/System.php on line 206
PHP Stack trace:
PHP   1. _PEAR_call_destructors() /usr/share/php/PEAR.php:0
PHP   2. call_user_func_array(array (0 => 'System', 1 => '_removeTmpFiles'), array ()) /usr/share/php/PEAR.php:774
PHP   3. System::_removeTmpFiles() /usr/share/php/PEAR.php:0
PHP   4. System::rm($args = array (0 => '-r', 1 => '/tmp/glibctestQDzKXJ')) /usr/share/php/System.php:440
PHP   5. unlink('/tmp/glibctestQDzKXJ') /usr/share/php/System.php:206

ああ、

1523                     $this->_explicitGroup = true;
1524                 }
1525             }
1526 
1527             if (@is_file($param)) {
1528                 $this->_type = 'local';
1529                 $options = $this->_downloader->getOptions();
1530                 if (isset($options['downloadonly'])) {
1531                     $pkg = &$this->getPackagefileObject($this->_config,
1532                         $this->_downloader->_debug);

@ 使ってるけど、僕の環境は xdebug.scream が On になってるから出るのね。PEARってほんとつくづく気持ち悪いよね。結構大きくなっててもう変えるのも直すのもだるいって話なんでしょうか(どこかで聞いたような話)。PEAR2に期待ってことですかね。。

エラーは出るけど @ つけて見えなくしてるから別にいいよねってかんじなんでしょうか。そんなやつは一度アレ *1を読みなおしてくると良いと思います。

追うの面倒だし、一応インストールはできてるから無視してる、が、気持ち悪い。

(とつぶやくだけのエントリーです。解決を期待した方すみません)

*1:え、この記事もう一昨年記事!?時間進みすぎじゃないすか?

名前空間とautoload、標準的なClassLoaderの実装 (#phpadvent2010)

ってことで、アドベントカレンダーが回ってきたので書きます。なに書こうか迷いましたが、とりあえず、最近自分でも真面目に使い始めたPHP 5.3向けのClassLoaderと名前空間についての話です。
世の中的にずいぶん「これからはPHP 5.3だよね」的な流れがきているので、名前空間の区切りと、ディレクトリ構成、ファイル名、クラス名など、これから書くならどうするんだろ?ってところについておさらいしておきます。

PHP Standards Working Group

第1回のモダンPHP勉強会で、id:Fivestarが発表したように、Symfonyなどの開発者が集まって、このような内容をPHP界隈でちゃんと取り決めて標準っぽくしようよって話をしている、PHP Standards Working Groupというグループがあります。(最近あんま動きがないな)

このグループで議論がまとまっているものに、PSR-0 Final Proposal があり、この中で、名前空間の使い方・クラス名・ディレクトリの区切りはこうしよう、という意見がまとまってます。*1

例をあげると、以下のような感じ。

  • \Doctrine\Common\IsolatedClassLoader => /path/to/project/lib/vendor/Doctrine/Common/IsolatedClassLoader.php
  • \Symfony\Core\Request => /path/to/project/lib/vendor/Symfony/Core/Request.php
  • \Zend\Acl => /path/to/project/lib/vendor/Zend/Acl.php
  • \Zend\Mail\Message => /path/to/project/lib/vendor/Zend/Mail/Message.php

ちなみにこのページはid:hnwさんが翻訳されています。

SplClassLoader を使ってみよう!

で、本題。標準的な名前空間とクラス名などの命名規則が決まれば、標準的なAutoloadクラスが作れるよねってことで作られたのが、SplClassLoaderです。SPLって名前が付いてるけど、別に組み込みでもないし、PHP的に標準になっているわけでもないので、「提案」レベルのものですが、SymfonyもこれをベースにClassLoaderをもっているし、まあ実質標準的なものであると考えてもらって問題ないです。

ってことで、これを使ってみましょう。

標準的なディレクトリ構成とクラスのファイルを用意

今回は、以下のような感じで作ってみました。

%  tree
.
|-- Hoge
|   |-- Hogege.php
|   `-- Hogera
|       `-- Aho.php
|-- SplClassLoader.php
|-- entry_point.php
`-- libs
    `-- vendors
        `-- Fuga
            `-- Foo.php

entry_point.php が実際にライブラリを使う側のファイル、Hoge と Fuga が、読み込まれる側のライブラリファイルだと想定し、Fuga はどこか変なとこにディレクトリがあるものだとします。まあ、よくある構成だはと思います。Aho.php Hogera.php Foo.php には実際にクラスが定義されていて、とりあえずコンストラクタが呼ばれると自分自身の名前空間とクラス名を出力するようにしてあります。
例えば次のような感じ:

<?php

namespace Hoge\Hogera;

class Aho
{
    public function __construct()
    {
        echo __CLASS__, PHP_EOL;
    }
}
実際に使ってみる

entry_point.php では、次のよう register() メソッドを呼び出し、名前空間とディレクトリのルートを指定したものを登録します。

<?php

require 'SplClassLoader.php';

$class_loader_hoge = new SplClassLoader('Hoge', __DIR__);
$class_loader_hoge->register();
$class_loader_fuga = new SplClassLoader('Fuga', __DIR__ . '/libs/vendors');
$class_loader_fuga->register();

$a = new Hoge\Hogege();
$b = new Hoge\Hogera\Aho();
$c = new Fuga\Foo();

この出力は、

Hoge\Hogege
Hoge\Hogera\Aho
Fuga\Foo

となります。
正しくHogeやFugaの名前空間のクラスが読み込まれましたね。

ClassLoaderの動きを追ってみる

ClassLoader の実装は簡単で、一言で説明すると、

登録された名前空間とパスを結合して \ を / に変えて .php を付ける

これだけです。

では、

$class_loader_hoge = new SplClassLoader('Hoge', __DIR__);
$class_loader_hoge->register();

この設定でAutoloadの設定をし、

$b = new Hoge\Hogera\Aho();

こう使ったときに、どう読み込まれるかというと、

<?php

// 以下は、SplClassLoaderのロード部分の実装。
    public function loadClass($className)
    {
        if (null === $this->_namespace || $this->_namespace.$this->_namespaceSeparator === substr($className, 0, strlen($this->_namespace.$this->_namespaceSeparator))) {
            $fileName = '';
            $namespace = '';
            if (false !== ($lastNsPos = strripos($className, $this->_namespaceSeparator))) {
                // 名前空間区切りが見つかれば、それを最後の名前空間区切り以下をクラス名として、
                // クラス名と名前空間に分解する
                $namespace = substr($className, 0, $lastNsPos);
                // => "Hoge\Hogera"
                $className = substr($className, $lastNsPos + 1);
                // => "Aho"

                $fileName = str_replace($this->_namespaceSeparator, DIRECTORY_SEPARATOR, $namespace) . DIRECTORY_SEPARATOR;
                // 名前空間の区切り文字 "\" を "/" に変換して、パス名にする
                // => "Hoge/Hogera/"
            }
            $fileName .= str_replace('_', DIRECTORY_SEPARATOR, $className) . $this->_fileExtension;
            // クラス名のアンダースコア "_" を "/" に変換して、拡張子をつける
            // そしてそれを、パス名に結合し、これをファイル名とする
            // => "Hoge/Hogera/Aho.php"
            require ($this->_includePath !== null ? $this->_includePath . DIRECTORY_SEPARATOR : '') . $fileName;
            // 最後に、設定してあるインクルードパスにファイル名を結合してrequireする
            // => "/home/sotarok/tmp/splclassloader/Hoge/Hogera/Aho.php"
        }
    }

だいたいこんなかんじで、SplClassLoaderのコンストラクタに指定したインクルードパスと、それ以下のパスが名前空間を含んだクラス名とマッピングされ、クラスが読み込まれます。実に単純ですね。

まんまでも使えるけど・・・

実際には、もうちょっとルーズに使いたかったので、registerNamespace()とかのstaticメソッドを定義して使ってます。*2


こうすると、

<?php

ClassLoader::registerNamespace('Hoge', '/path/to/Hoge');
ClassLoader::registerNamespace('Fuga', '/path/to/Fuga');

なんてかんじでルーズに使えて素敵です。

というわけで

いよいよ、と思って名前空間を使いはじめると、まだまだノウハウが共有されていなくて(それでも出てきたほうですが)悩むことも多いと思いますが、参考になれば幸いです。

アドベントカレンダーのバトンは slumbers99 さんにお渡しします!

PHP 5.3の話もちゃんと説明している、パーフェクトPHPもよろしくね!

*1:そういや、Google Groupsってページ機能をサポートしなくなるっぽいけど、このへんのページはどうなるんだろうか・・・

*2:file_exists使うとstatが発生するから、コスト的になーってやつがありますよね、まあ、ErrorException飛ばすようにしろよとかいろいろありますが、個人的にはこれでしっくりくるかんじ