pharのしくみ
PHP5から追加された機能として、SimpleXMLとかSPLなど、動作的にユーザレベルでは実現できないおもしろい動きをするものがあります。5.3から標準モジュールとなったpharも同じように結構見えないところでPHP本体の動きを変えるような実装をしているので、調べてみました。
以下の部分に大きく分けています。
- stream wrapperの定義
- zend_compile_fileの差し替え
- ファイルアクセス系関数の差し替え
- パス解決の差し替え
stream wrapperの定義
pharは、stream wrapperとして動くことで、"phar://path/to/foo.phar/bar.php" といったファイルにアクセスすることができます。
普段よく使う http://〜 、 https://〜 、ftp:// などと同様で、抽象化してアクセスできるようになるしくみです。
定義
stream.c の中で構造体が宣言されています。見てわかるように、各ファイル操作に対して、関数を定義しています。
// phar/stream.c 25 php_stream_ops phar_ops = { 26 phar_stream_write, /* write */ 27 phar_stream_read, /* read */ 28 phar_stream_close, /* close */ 29 phar_stream_flush, /* flush */ 30 "phar stream", 31 phar_stream_seek, /* seek */ 32 NULL, /* cast */ 33 phar_stream_stat, /* stat */ 34 NULL, /* set option */ 35 }; 36 37 php_stream_wrapper_ops phar_stream_wops = { 38 phar_wrapper_open_url, 39 NULL, /* phar_wrapper_close */ 40 NULL, /* phar_wrapper_stat, */ 41 phar_wrapper_stat, /* stat_url */ 42 phar_wrapper_open_dir, /* opendir */ 43 "phar", 44 phar_wrapper_unlink, /* unlink */ 45 phar_wrapper_rename, /* rename */ 46 phar_wrapper_mkdir, /* create directory */ 47 phar_wrapper_rmdir, /* remove directory */ 48 }; 49 50 php_stream_wrapper php_stream_phar_wrapper = { 51 &phar_stream_wops, 52 NULL, 53 0 /* is_url */ 54 }; 55
モジュール初期化時
pharのstream wrapperの定義をします。これで、"phar" から始まるストリームにアクセスする際に、各関数が呼ばれます。
// phar/phar.c 3530 PHP_MINIT_FUNCTION(phar) /* {{{ */ 3531 { ... 3550 return php_register_url_stream_wrapper("phar", &php_stream_phar_wrapper TSRMLS_CC); 3551 }
読み込みの部分は長ったらしいので省略。
phar://でpharファイルにアクセスするときには、phar_wrapper_open_url()が呼ばれ、開いたあとは、php_stream_allocでストリームの動作の定義をします。
fpf = php_stream_alloc(&phar_ops, idata, NULL, mode);
zend_compile_fileの差し替え
モジュール初期化時に、zend_compile_file をpharのものに差し替えています。
// phar/phar.c 3530 PHP_MINIT_FUNCTION(phar) /* {{{ */ 3531 { ... 3534 phar_orig_compile_file = zend_compile_file; 3535 zend_compile_file = phar_compile_file; ... 3551 }
pharへアクセスするのは、stream wrapperが役目を担っています。include "http://example.com" とかできるのと同じ仕組みです。
compile_fileでは、include/requireされたスクリプトをコンパイルする役割を持ちます。pharのアーカイブからスクリプトを取り出す部分は、stream wrapperがやってくれるので、普通は、ここを差し替える必要はありません。また、スタブ部分はpharアーカイブの先頭から__halt_compiler()までの部分に含まれるので、普通にincludeするだけで実行されます。
ここを差し替えているのは例えばAPCなどのコンパイルキャッシュ系のモジュールです。ファイルにアクセスしてコンパイルする代わりに、キャッシュしたコンパイル結果を返します。
では、ここで何を処理しているかというと、include/requireの引数のファイルパスに .phar を含み且つ、 :// を含まないファイルについて読み込みを行い、圧縮ファイルであった場合にpharとして読み込み処理を行っています。
なので、include "/path/to/archive.phar.bz2" とか、
php hogehoge.phar.bz2
とかコマンドラインで圧縮されたpharを起動する場合に対応します。
3329 static zend_op_array *phar_compile_file(zend_file_handle *file_handle, int type TSRMLS_DC) /* {{{ */ 3330 { ... 3339 if (strstr(file_handle->filename, ".phar") && !strstr(file_handle->filename, "://")) { 3340 if (SUCCESS == phar_open_from_filename(file_handle->filename, strlen(file_handle->filename), NULL, 0, 0, &phar, NULL TSRMLS_CC)) { 3341 if (phar->is_zip || phar->is_tar) { 3342 zend_file_handle f = *file_handle; ... 3358 } else if (phar->flags & PHAR_FILE_COMPRESSION_MASK) { ... 3384 } 3385 } 3386 } 3387 3388 zend_try { 3389 failed = 0; 3390 res = phar_orig_compile_file(file_handle, type TSRMLS_CC); 3391 } zend_catch { 3392 failed = 1; 3393 } zend_end_try();
パス解決の差し替え
phar://〜 で、pharの中のファイルを読み込めても、そのpharの中から相対パスで require 'hoge.php' などと書かれていたら、そのままでは普通にファイルを探そうとしてしまいます。そのため、PHPがパスを解決する部分に手が入れられています。
MINIT (拡張モジュールが初期化されるときに呼ばれる関数)で、pharのモジュールの初期化のタイミングで以下のことをやっています。
- PHP5.3以降: zend_resolve_path の差し替え
- PHP5.3以前: zend_stream_open_functionの差し替え
// phar/phar.c 3530 PHP_MINIT_FUNCTION(phar) /* {{{ */ 3531 { ... 3537 #if PHP_VERSION_ID >= 50300 3538 phar_save_resolve_path = zend_resolve_path; 3539 zend_resolve_path = phar_resolve_path; 3540 #else 3541 phar_orig_zend_open = zend_stream_open_function; 3542 zend_stream_open_function = phar_zend_open; 3543 #endif ... 3551 }
pharのアーカイブ内で、相対パスで同じくアーカイブ内のファイルをincludeするようなことがあると思います。
たとえば、図のような構成になっているpharアーカイブのとき。
pharアーカイブの中で require '1.php' と同じ階層のファイルをrequireしたり、require '../2.php' とディレクトリを辿るような場合、通常のパス解決をすると、当然ファイルが見つかりません。これを解決するための実装で、PHP本体の仕様が異なることからバージョンに応じて変えている感じです。
[PHP5.3以上] zend_resolve_pathの差し替え
PHP5.3以降では、zend_resolve_pathをpharのものに差し替えています。zend_resolve_pathは、include pathを参照して、渡されたfilenameのrealpathを返却する関数です。(main/fopen_wrappers.cを参照)
これを phar_resolve_path という関数に差し替え、もし、requireなどをしている元のファイルが pharの中である場合に特殊動作をし、相対パスを絶対パスの phar:///path/to/archive.phar/1.php と展開することでファイルの参照を可能としています。
pharで差し替えた関数では、
3321 static char *phar_resolve_path(const char *filename, int filename_len TSRMLS_DC) 3322 { 3323 return phar_find_in_include_path((char *) filename, filename_len, NULL TSRMLS_CC); 3324 }
と、phar内の相対パスを良きに計らう関数に渡して、パス解決をしています。
[PHP5.3未満] zend_stream_open_functionの差し替え
5.3以前では、zend_stream_open_function を同じく差し替えています。これは、ストリームをオープンするための関数です。同じく、phar_find_in_include_path を呼んでパス解決しています。
ファイルアクセス関数の差し替え
pharには、Phar::interceptFileFuncsという、ファイルアクセス関連の関数をpharに横取りさせるメソッドがあります。
void Phar::interceptFileFuncs(void)fopen() や readfile()、 file_get_contents()、opendir() などの stat 関連の関数をすべて phar に横取りさせます。 phar アーカイブ内で相対パスを指定してこれらの関数がコールされると、 それが phar アーカイブ内のファイルへのアクセスに変更されます。 絶対パスの場合は、ファイルシステム上の外部ファイルを指すものとみなされます。
PHP: Phar::interceptFileFuncs - Manual
このメソッドを実現するため、モジュール初期化時に元の関数をphar関数に差し替えます。関数を呼んだときに差し替えるのではなく、最初に全部差し替えちゃっている感じです。
// phar/phar.c 3530 PHP_MINIT_FUNCTION(phar) /* {{{ */ 3531 { ... 3547 phar_intercept_functions_init(TSRMLS_C); 3548 phar_save_orig_functions(TSRMLS_C); ... 3551 }
1つ目のphar_intercept_functions_init では、マクロでざっくりと、ファイルアクセス系関数を差し替えます。fopen を PHAR_G(orig_fopen) に移動し、元の fopen を phar_fopen に差し替えという感じに。
//func_interceptors.c 1056 #define PHAR_INTERCEPT(func) \ 1057 PHAR_G(orig_##func) = NULL; \ 1058 if (SUCCESS == zend_hash_find(CG(function_table), #func, sizeof(#func), (void **)&orig)) { \ 1059 PHAR_G(orig_##func) = orig->internal_function.handler; \ 1060 orig->internal_function.handler = phar_##func; \ 1061 } 1062 1063 void phar_intercept_functions_init(TSRMLS_D) 1064 { 1065 zend_function *orig; 1066 1067 PHAR_INTERCEPT(fopen); 1068 PHAR_INTERCEPT(file_get_contents); ... 1087 PHAR_INTERCEPT(stat); 1088 PHAR_INTERCEPT(readfile); 1089 PHAR_G(intercepted) = 0; 1090 }
2つ目のphar_save_orig_functions では、上の関数で PHAR_G に突っ込んだ関数を phar_orig_functions に突っ込み直しているだけ。
1129 static struct _phar_orig_functions { 1130 void (*orig_fopen)(INTERNAL_FUNCTION_PARAMETERS); 1131 void (*orig_file_get_contents)(INTERNAL_FUNCTION_PARAMETERS); ... 1150 void (*orig_readfile)(INTERNAL_FUNCTION_PARAMETERS); 1151 void (*orig_stat)(INTERNAL_FUNCTION_PARAMETERS); 1152 } phar_orig_functions = {NULL}; 1153 1154 void phar_save_orig_functions(TSRMLS_D) /* {{{ */ 1155 { 1156 phar_orig_functions.orig_fopen = PHAR_G(orig_fopen); 1157 phar_orig_functions.orig_file_get_contents = PHAR_G(orig_file_get_contents); ... 1176 phar_orig_functions.orig_readfile = PHAR_G(orig_readfile); 1177 phar_orig_functions.orig_stat = PHAR_G(orig_stat); 1178 }
元の関数から差し替えている先の phar_fopen などの関数はこういう実装になっていて、interceptがオフのときは、さっくりとgotoして、元の関数を呼び出します。
26 PHAR_FUNC(phar_opendir) /* {{{ */ 27 { 28 char *filename; 29 int filename_len; 30 zval *zcontext = NULL; 31 32 if (!PHAR_G(intercepted)) { 33 goto skip_phar; 34 } 35 36 if ((PHAR_GLOBALS->phar_fname_map.arBuckets && !zend_hash_num_elements(&(PHAR_GLOBALS->phar_fname_map))) 37 && !cached_phars.arBuckets) { 38 goto skip_phar; 39 } /* ここで仮想的にファイル処理 */ 88 skip_phar: 89 PHAR_G(orig_opendir)(INTERNAL_FUNCTION_PARAM_PASSTHRU); 90 return; 91 }
まとめ
今のところあんまりぱっとしないPharですが、思った以上にpharを動かすために PHP内部に特殊な動きを組み込んでいることが分かった感じです。
使わないともったいないですね!というのと、phar使わないから無駄な処理省きたいという場合は、phar拡張を抜くというのもありかと思います。そんなに変わらないと思いますが。