thephpleague/routeのミドルウェアのコードを読んでいたらクロージャの中で次の関数を呼び出して繋げるという処理を見つけました。何故、登録した関数の間に次の関数を呼び出すだけのクロージャを挟み込むのかを考えます。
今回はテーマを元に、ミドルウェアのコードを書き換えました。ミドルウェアは関係なく、ただ関数を繋げるだけなので引数に$respone
, $request
は使わずに、$lines
(array
)だけ使います。$lines
は関数内で出力を入れるものです。どのような順番で関数が呼ばれているかをチェックします。
下記は使用例です。
<?php
// 関数を登録して、実行するクラスを生成します。
$chain = new ExecutionChain;
// 関数を登録します。最後の$nextは次に呼び出す関数です。
$chain->addFunc(function (array $lines, callable $next) {
$lines[] = 'func 1';
return $next($lines);
});
$chain->addFunc(function (array $lines, callable $next) {
$lines[] = 'func 2';
return $next($lines);
});
$lines = [];
$respone = $chain->execute($lines);
print_r($respone);
/* =>
Array
(
[0] => func 2
[1] => func 1
[2] => last func
)
*/
登録した順番とは逆で呼ばれていきますね。addFunc
で登録するということは、既存の関数を外側から包み込むということです。ラッパー関数になります。最後のinit func
はExecutionChain
内で予め用意したものです。$next
が1つ内部で作られているということです。
上記の実装がこちらです。
<?php
/* 01_simple_format.php */
class ExecutionChain
{
protected $funcs = [];
public function addFunc(callable $func)
{
$this->funcs[] = $func;
return $this;
}
public function execute(array $lines)
{
$chain = $this->buildExecutionChain();
// $chainは1番外側の関数です。
// 最後にaddFuncした関数のラッパー関数です。addFuncしたものではありません。
return $chain($lines);
}
protected function buildExecutionChain(): callable
{
// 最後に実行される関数です。
$next = function (array $lines) {
$lines[] = 'last func';
return $lines;
};
foreach ($this->funcs as $func) {
$next = function (array $lines) use ($func, $next) {
return $func($lines, $next);
};
}
return $next;
}
}
execute
するたびに登録した関数が繋げられて1つの関数になります。では、メインのbuildExecutionChain
のforeach
を見ていきます。下記はforeach
のループを明示的に書き出したものです。
説明の便宜上、登録した関数を呼び出すラッパー($next
)をcall_~
と呼ぶことにします。buildExecutionChain
内で最初に作られた関数をf_last
と呼びます。これらは全部$next
に入るものです。
$next
とはaddFunc
した関数ではないことに注意です。addFunc
した関数を呼ぶための関数です。このforeach
で新たに生成されているラッパー関数です。ただし、例外として$f_last
が最初に$next
に入っています。
<?php
// 最初の$nextはlast funcです。$nextを使わないことに注意です。
foreach ([$f1, $f2] as $func) {
$next = function (array $lines) use ($func, $next) {
return $func($lines, $next);
};
}
// $f1を呼び出す関数 call_f1
$next = function (array $lines) use ($func, $next) {
return $f1($lines, $next); // $next == $f_last
};
// $f2を呼び出す関数 call_f2
$next = function (array $lines) use ($func, $next) {
return $f2($lines, $next); // $next == $f1を呼び出す関数
};
この場合だと、$next
は最終的にcall_f2
が入っています。これがbuildExecutionChain
の戻り値です。$chain->execute($lines);
は、外側から引数($lines
)をcall_f2
に渡して実行することになります。これがどのように繋がって呼び出されるかをコードで見てみます。call_~
は、buildExecutionChain
内で作られたクロージャです。
<?php
public function execute(array $lines)
{
$chain = $this->buildExecutionChain();
return $chain($lines);
}
// $chainの処理は下記の通りです。
// $call_f2
function (array $lines) {
return $f2($lines, $call_f1); // $call_f1は$f1を呼び出す関数
};
// $f2
function (array $lines, callable $next) {
$lines[] = 'func 2';
return $next($lines); // $next == $call_f1
}
// $call_f1
function (array $lines) {
return $f1($lines, $f_last); // $f_lastは$nextを実行しない
}
// $f1
function (array $lines, callable $next) {
$lines[] = 'func 1';
return $next($lines); // $next == $f_last
}
// $f_last
function (array $lines) {
$lines[] = 'last func';
return $lines;
};
$next
がcall_~
を呼び出していますね。$next
を呼び出せば次の関数に繋げられることになります。また、全て関数内で次の関数の結果をreturn
しているので、call_f2
の戻り値は$f_last
の戻り値になります。戻り値も繋がっているわけです。
クロージャをなくせばシンプルに書けるのでは?
何故、関数を繋げるのにラッパー関数($next
)を挟み込んでいるのでしょうか?関数を繋げるだけならforeach
内でクロージャを作らなくても実現可能です。下記は、buildExecutionChain
を無くしてexecute
内で処理をまとめたものです。print_r
の結果は先程と同じになります。
<?php
/* 02_no_call_next.php */
public function execute(array $lines)
{
$last_func = function (array $lines) {
$lines[] = 'last func';
return $lines;
};
// ラッピングしないので、配列の順番を逆にします。
foreach (array_reverse($this->funcs) as $func) {
$lines = $func($lines);
}
return $last_func($lines);
}
// 登録する時に$nextを考える必要はなくなります。
$chain->addFunc(function (array $lines) {
$lines[] = 'func 1';
return $lines;
});
$chain->addFunc(function (array $lines) {
$lines[] = 'func 2';
return $lines;
});
関数で関数を包み込んでいるわけではないので、ラッパー関数ではないですが、テーマである関数の連結は出来ています。戻り値も次の関数の引数として渡せています。なにより、コードが先程のクロージャと比べてシンプルです。
ですが、これだと次のことが出来ないです。
- 次の関数の呼び出しの後に処理を挟み込むことができる。
- 場合によって次の関数に繋げない。(
$next
を呼ばない)
言い換えれば、これらがクロージャで実現可能です。
<?php
// $nextがあるので、次の関数を呼び出した後にも処理を書くことができます。
$chain->addFunc(function (array $lines, callable $next) {
echo 'before';
$lines[] = 'func 1';
$response = $next($lines);
echo 'after';
return $respone;
});
// $nextがない場合だと、returnすると絶対に次に関数が呼ばれてしまいます。
$chain->addFunc(function (array $lines, callable $next) {
if (empty($lines)) {
return false;
}
$lines[] = 'func 1';
return $next($lines);
});
そうすると下記の画像に矢印が2つあり、入り口と出口が違うことが分かります。ミドルウェアとはaddFunc
した関数のことで、アプリの包み込むラッパー関数のことです。ミドルウェアを使って、ログインしている場合のみ、アプリに回すということが可能です。
クロージャを使わない例だと、後処理が挟み込めないので来た道を戻ることになるので、startからの矢印1本だけになります。両端が三角になっているということです。それがクロージャだと後処理が挟み込めるので、アプリから出る時に違う道から出ることが可能になるわけです。
このようにクロージャーのおかげで、ミドルウェアで$response
を処理することが可能になりました。