1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

連結する関数の間にクロージャーを挟み込む理由を、ミドルウェアの実装から知る。

Last updated at Posted at 2017-01-29

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 funcExecutionChain内で予め用意したものです。$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つの関数になります。では、メインのbuildExecutionChainforeachを見ていきます。下記は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;
};

$nextcall_~を呼び出していますね。$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した関数のことで、アプリの包み込むラッパー関数のことです。ミドルウェアを使って、ログインしている場合のみ、アプリに回すということが可能です。

Slimのドキュメントからの引用です。

middleware.png

クロージャを使わない例だと、後処理が挟み込めないので来た道を戻ることになるので、startからの矢印1本だけになります。両端が三角になっているということです。それがクロージャだと後処理が挟み込めるので、アプリから出る時に違う道から出ることが可能になるわけです。

このようにクロージャーのおかげで、ミドルウェアで$responseを処理することが可能になりました。

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?