Edited at
PHPDay 18

PHPライブラリを理解する喜びを味わう

More than 3 years have passed since last update.


はじめに

'24 Cool PHP Libraries You Should Know About'というサイトにあった'shell wrap'を読み込んだらかなり勉強になったので紹介します。「PHPでプログラムは書けるけど、もうちょっとレベルアップしたい。Linuxについても知りたい。コンポーザーとか名前空間とか、よく分からない」という方向けに書いたつもりです。

などについて、ざっくりと理解できると思います。それぞれのリンクは私が参考にしたページです。すいませんが、一つ一つを詳しく解説できませんので、冬休みも近いですし、頑張って参考ページを読んで下さい。


ライブラリの使い方

まずはライブラリを使えるようにします。shellwrapはコンポーザーを使ったライブラリ管理を前提にしているので、コンポーザーの使い方も見ていきます。まず、GitHubの"Download Zip"を右クリックしてリンク先を取得して、wgetでダウンロードします。なお、動作環境はdebianで確認しています。

> wget https://github.com/MrRio/shellwrap/archive/master.zip

> unzip master.zip

これでプロジェクトをダウンロードできました。次に、コンポーザーをダウンロードしましょう。

> cd shellwrap-master/

> curl -sS https://getcomposer.org/installer | php
Downloading...
Composer successfully installed to: ~/composer.phar
Use it: php composer.phar

ここでダウンロードしたcomposer.pharは別のプロジェクトでも使えますが、毎回最新をダウンロードする事が推奨されています。つぎに、composer.pharを使って、プロジェクトを実行する為に必要な環境をセットアップします。

> ./composer.phar install

この時、コンポーザーは~/shellwrap-master/composer.jsonに記述された設定に従って、環境をセットアップします。composer.jsonはプロジェクトの作者が作っています。

では、example.phpを実行してみましょう。

> cd ../examples

> ls
example.php grep.php ls.php
> php ls.php
example.php
grep.php
ls.php

これで実行できました。ls.phpの一行目を見ると、

require_once __DIR__ . '/../vendor/autoload.php';

と書いてあります。composerを使わないと、autoload.phpは生成されません。autoload.phpは必要なライブラリをインクルードする為の仕組みです。ここでは、コンポーザーを使うと、autoload.phpを生成してくれるんだなと理解しておいて下さい。


ライブラリの処理をざっくり理解する

まずは、ls.phpを実行してみます。プログラムはこちら。

<?php

require_once __DIR__ . '/../vendor/autoload.php';
use MrRio\ShellWrap as sh;
echo sh::ls();
?>

一行目はオートロードでしたね。二行目は利用するクラスの定義です。ここで実際のクラスを定義しているShellWrap.phpの先頭に次のように定義されている事に注意して下さい。

namespace MrRio;

これは、このファイルのクラス、関数、定数などの定義は、MrRioという名前空間を使いますよ、という意味です。つまり、クラスの名前はShellWrapですが、これを使う時は、MrRio\ShellWrapを使って下さいと言っています。なぜこんな事をするかと言うと、この広い世界のどこかに、ShellWrapというクラスを作る人がいるかもしれません。そうすると、この二つのクラスは同時に利用できません。名前空間でそれを回避できます。

もう一度、ls.phpの記述に戻ります。

use MrRio\ShellWrap as sh;

これは、MrRio\ShellWrapクラスを使いますよという意味です。ただ、プログラム上で毎回MrRio\ShellWrapと書くのは面倒なので、as shと書いて、shという名前でクラスを使いますと宣言しています。

次に実際の動作です。sh::ls()をコールすると、ShellWrap::__callStaticが呼ばれます。これはオーバーロードメソッドの一種で、静的コンテキストで実行したメソッドがアクセス不能な時に実行されます。ざっくり言うと、Class::Method()のように呼び出したとき、Medhod()が存在しない場合は、Class::__callStatic()がコールされるという事です。

    public static function __callStatic($name, $arguments)

{
//ここで$name='ls',arguments=array()
array_unshift($arguments, $name);
//ここでself::$prpend=null
if (isset(self::$prepend)) {
$arguments = array_merge(self::$prepend, $arguments);
}
//ここで$argument=array('ls)
self::__run($arguments);
return new self();
}

次に__run()の動作を見ていきます。proc_openのマニュアルは一読しておく事をお勧めします。

    private static function __run($arguments)

{
/* 省略 */
//この時点で$arguments=array('ls')
$shell = implode(' ', $arguments);

/* 省略 */
// この時点で$shell = 'ls'
$parts = explode(' ', $shell);

// whichでコマンドのパス取得
$parts[0] = exec('which ' . $parts[0]);

if ($parts[0] != '') {
$shell = implode(' ', $parts);
}
// この時点で$shell = '/bin/ls'

//proc_open結果の出力先を設定します
$descriptor_spec = array(
0 => array('pipe', 'r'), // Stdinはパイプに渡す
1 => array('pipe', 'w'), // Stdoutはパイプに渡す
2 => array('pipe', 'w') // Stderrはパイプに渡す
);

//シェルコマンド'/bin/ls'を実行する準備が出来ました。
$process = proc_open($shell, $descriptor_spec, $pipes);

if (is_resource($process)) {
//シェルコマンドにself::$stdinの値を渡してコマンドを実行します。この時点ではnullです。
//つまり、/bin/lsが実行されます。
fwrite($pipes[0], self::$stdin);
fclose($pipes[0]);

//stdout(つまり上記のコマンドの実行結果)を$outputに加えていきます。
$output = '';
while(! feof($pipes[1])) {
         //コマンド結果の出力をgetします。
$stdout = fgets($pipes[1], 1024);
if (strlen($stdout) == 0) break;
/* 省略 */
$output .= $stdout;
}
//この時点でのself::$output='example.php\ngrep.php\nls.php' (カレントフォルダのファイル名)
self::$output = $output;
/* 省略 */
} else {
throw new ShellWrapException('Process failed to spawn');
}
//exec($shell, $output, $return_var);
}

$shell = implode(' ', $arguments);の前の処理は、コマンドにオプションを付けたり、(後で解説する)パイプ処理などをしています。ここでは特に$argumentsの値は大きく変わらないので、特に説明はしません。self::$outputの値がls()と同じになる事を押さえておいて下さい。

run()が実行された後、__callStatic()の最後の行が実行されます。

        return new self();

ここでnew self()としているのがミソです。これまでの処理で決まったstatic変数はすべて保存されて状態で、オブジェクトが返されます。つまりここで返しているオブジェクトのself::$outputは保存されています。一方、メイン処理では

echo sh::ls();

となっています。echoした時に何が起きるかというと、ShellWrap::__toString()が実行されます。toString()もマジックメソッドの一種で、クラスが文字列に変換される際の動作を決めます。ではShellWrapではどうしてるかというと、、、

    public function __toString()

{
return self::$output;
}

self::$outputを返しています。つまり、self::$output='example.php\ngrep.php\nls.php' の値が返ります。従って実行結果は

example.php

grep.php
ls.php

となります。

ここまで、いかがだったでしょうか?proc_openのあたりがややこしいかもしれません。もし良く分からなかったら、標準入力などについて再度確認しつつ、PHPのマニュアルを読んでみて下さい。正直、私もproc_openは今回初めて知ったので、もし理解が不十分な点があったら、指摘して下さい。

次はもう少し複雑な処理を見ていきます。先ほどの例と同じフォルダで実行していると仮定して下さい。

<?php

require_once __DIR__ . '/../vendor/autoload.php';
use MrRio\ShellWrap as sh;
echo sh::grep('example',sh::ls());
?>

この場合は引数が二つです。二つ目の引数がメソッドになっているので、run()を実行した時のargumentが次のようになっています。

Array

(
[0] => grep
[1] => example
[2] => MrRio\ShellWrap Object
(
)

)

ここでArray[2]はObjectになっていますが、self::$outputには

example.php

grep.php
ls.php

が格納されていることに注意して下さい。この場合のrunの処理は

    private static function __run($arguments)

{
self::$stdin = null;
$closureOut = false;
foreach ($arguments as $arg_key => $argument) {
if (is_object($argument)) {
if (get_class($argument) == 'Closure') {
$closureOut = $argument;
unset($arguments[$arg_key]);
} else {
// array[2]はここで評価されます。つまり、ls()の結果が文字列として格納されます。
self::$stdin = strval($argument);
unset($arguments[$arg_key]);
}
} elseif (is_array($argument)) {
/* 省略 */
}
}

/* 省略 */
// ここでの$shell='grep example'
$process = proc_open($shell, $descriptor_spec, $pipes);

if (is_resource($process)) {
//ls()コマンドの結果が標準入力に渡されてgrepが実行されます。※1
fwrite($pipes[0], self::$stdin);
fclose($pipes[0]);
/* 省略 */

array[2]がstrvalで評価され、stdinに入っています。この後※1で実行されるのは、次のような処理とほぼ等価です。

echo 'example.php' > self_stdin

echo 'grep.php' >> self_stdin
echo 'ls.php' >> self_stdin
grep example < self_stdin

こんな感じでパイプの処理を実現しています。賢いですね。よく出来たコードはモジュールごとのインターフェースがうまく出来ていると言われますが、それを実感できます。もしパイプ処理の部分がよく分からなかったら、もう一度見直す事をお勧めします。

ちなみに、GitHubにはclosureを使った次のような例がありますが、この部分の評価はif (get_class($argument) == 'Closure')の後でやっています。

sh::tail('-f log', function($in) {

echo "\033[32m" . date('Y-m-d H:i:s') . "\033[39m " . $in;
});

ここまで駆け足ですが、いかがでしょうか?前提の知識が無いと、よく分からないかもしれません。ただ、一つ一つ丁寧に理解すれば大丈夫だと思います。他人の書いたソースコードを理解する事の喜びを知る手助けに、少しでもなれば有難いです。よく分からない点、間違っている点などは指摘して下さい。