😎

ワイ「PHPのコールバック、完全に理解したで!」

2022/07/11に公開

この記事は、社内向けの勉強会で喋った内容を関西型言語で書き起こしたものです。

元々初級者を想定した内容だったのですが、
ベテラン勢も、意外と知らないところが多かったようですね。

第1章 array_map のおさらい

先生「今日はコールバックについてお勉強しましょう。」

ワイ(コールバックってあれやろ。引数の中に function とかごにょごにょ書くやつやろ。)
ワイ(あれごちゃごちゃしてるしわかりにくいし嫌いやねん。)

先生「PHPでコールバックの代表格といえば、array_map() ですね。」
先生「1番基本的な使い方はこんな感じです。」

$array = [1, 2, 3, 4, 5];

function double(int $item) : int
{
    return $item * 2;
}

$doubled = array_map('double', $array);

var_dump($doubled); // [2, 4, 6, 8, 10]

ワイ(あれ、function () {〜} とか出て来ぇへんのか。)
ワイ(数字を2倍にする double() 関数を作って、それを $array の全部にかけてるってことやな。)
ワイ(なんや、これならわかりやすいやんけ!)

先生「でも今どきグローバル関数なんてほとんど定義しないですよね。」
先生「こうすると、クラスのメソッドを呼び出すこともできます」

$array = [1, 2, 3, 4, 5];

class Calculator {
    public static function double(int $item) : int
    {
        return $item * 2;
    }
}

$doubled = array_map(['Calculator', 'double'], $array);

var_dump($doubled);

ワイ(な・・・['Calculator', 'double'] やと・・・)
ワイ(クラス名とメソッド名を配列で渡すってどういうことやねん。ローカルルールすぎるやろ!)

先生「そもそもこれが1度しか使わない処理だったら、わざわざ名前をつけて関数やメソッドにするのはどうかと思いますよね。」
先生「こうやって、関数を直接渡すこともできます」

$array = [1, 2, 3, 4, 5];

$doubled = array_map(function (int $item) {
    return $item * 2;
}, $array);

var_dump($doubled);

ワイ(ほら出た、function ごにょごにょ・・・)
ワイ(でも、最初のと見比べると、わかったかもしれへん)
ワイ(わざわざ double() みたいに名前をつけへんから、無名関数なんやな!)

先生「では、ここからが本題です。」

先生「ここまで登場した3つの array_map を my_map と書き換えてください」

// パターン1

$array = [1, 2, 3, 4, 5];

function double(int $item) : int
{
    return $item * 2;
}

$doubled = my_map('double', $array);

var_dump($doubled);


// パターン2

$array = [1, 2, 3, 4, 5];

class Calculator {
    public static function double(int $item) : int
    {
        return $item * 2;
    }
}

$doubled = my_map(['Calculator', 'double'], $array);

var_dump($doubled);


// パターン3

$array = [1, 2, 3, 4, 5];

$doubled = my_map(function (int $item) {
    return $item * 2;
}, $array);

var_dump($doubled);

ワイ「先生、Call to undefined function my_map() って怒られるで。」

先生「はい、では、my_map() を実装してください」

ワイ「え・・・」

先生「array_map() と同じような動きになるように、my_map() 関数を作ってみてください。」
先生「もちろん、内部で array_map() は使わずに。」

第2章 array_map を自作しよう

ワイ「どないすんねん、こんなもん。」
ワイ「いきなり複雑なものはよう書かへんから、一歩ずつ地道に行くで!」

ワイ「まずはここからや。」

// パターン1

$array = [1, 2, 3, 4, 5];

function double(int $item) : int
{
    return $item * 2;
}

$doubled = my_map('double', $array);

var_dump($doubled);

/**
 * array_map と同じ動きをする
 */
function my_map($function, $array)
{

}

ワイ「えーっと、$array の全部に double 関数をかけるんやから、」
ワイ「とりあえず foreach やろ。」

/**
 * array_map と同じ動きをする
 *
 * @todo double がハードコーディング
 */
function my_map($function, $array)
{
    $mapped = [];

    foreach ($array as $key => $value) {
        $mapped[$key] = double($value);
    }

    return $mapped;
}

ワイ「で、関数名 'double' が、第1引数に文字列で渡ってくるんやから、」
ワイ「たぶんこうやな!」
ワイ「タイプヒンティングもつけて・・・」

/**
 * array_map と同じ動きをする
 */
function my_map(string $function, array $array) : array
{
    $mapped = [];

    foreach ($array as $key => $value) {
        $mapped[$key] = $function($value);
    }

    return $mapped;
}

ワイ「よし、動いた!」
ワイ「ワイ天才!」

ワイ「あ、引数で渡された関数を、逆に呼び出すから、コールバックなんやな!」
ワイ「コールバックって、こっちの視点からのネーミングやったんや!」

ワイ「じゃあ次は・・・」

// パターン2

class Calculator {
    public static function double(int $item) : int
    {
        return $item * 2;
    }
}

$doubled = my_map(['Calculator', 'double'], $array);

var_dump($doubled);


/**
 * array_map と同じ動きをする
 *
 * @todo $function に配列もわたってくる
 */
function my_map(string $function, array $array) : array
{
    $mapped = [];

    foreach ($array as $key => $value) {
        $mapped[$key] = $function($value);
    }
    
    return $mapped;
}

ワイ「パターン2では、クラス名とメソッド名の配列もわたってくるんか・・・」
ワイ「第1引数が文字列のときと、配列のときで処理を分けなあかんな。」

/**
 * array_map と同じ動きをする
 *
 * @param string|array $function
 * @param array $array
 * @return array
 */
function my_map(mixed $function, array $array) : array
{
    $mapped = [];

    foreach ($array as $key => $value) {
        if (is_string($function)) {
	    $mapped[$key] = $function($value);
        } elseif (is_array($function)) {
	    /** @todo まだ */
        }
    }

    return $mapped;
}

ワイ「ややこしくなってきたで。」
ワイ「ええっと、Calculator::double($value) としたいわけで、」
ワイ「配列の1番目が Calculator、2番めが double やから、」
ワイ「こうやろか!?」

$mapped[$key] = $function[0]::$function[1]($value);
Fatal error: Uncaught Error: Access to undeclared static property Calculator::$function

ワイ「あかん、これやと $function が静的プロパティ扱いになってまうんやな。」
ワイ「カッコつけたらどないや!?」

/**
 * array_map と同じ動きをする
 *
 * @param string|array $function
 * @param array $array
 * @return array
 */
function my_map(mixed $function, array $array) : array
{
    $mapped = [];

    foreach ($array as $key => $value) {
        if (is_string($function)) {
	    $mapped[$key] = $function($value);
        } elseif (is_array($function)) {
	    $mapped[$key] = $function[0]::{$function[1]}($value);
        }
    }

    return $mapped;
}

ワイ「よっしゃ!動いた!」
ワイ「やっぱりカッコつけなカッコつかんな!」
ワイ「ワイ天才!!」

ワイ「問題はこいつや」

// パターン3

$array = [1, 2, 3, 4, 5];

$doubled = my_map(function (int $item) {
    return $item * 2;
}, $array);

var_dump($doubled);

ワイ「まず、 function を渡すってなんやねん。」
ワイ「引数の型はいったい何やねん・・・」
ワイ「せや、こんなときのための秘策、 var_dump() や!」

// テスト
var_dump(function (int $item) {
    return $item * 2;
});
object(Closure)#1 (1) {
  ["parameter"]=>
  array(1) {
    ["$item"]=>
    string(10) "<required>"
  }
}

ワイ「く・・・Closure オブジェクトやと・・・?」
ワイ「あかん、マニュアルやマニュアル!」

ワイ「あった、Closure クラス。」

ワイ「・・・PHPにこんなものがあったんかいな。」
ワイ「難しすぎてよーわからんけど、」
ワイ「とにかく call() メソッドで実行できるっぽいで。」

/**
 * array_map と同じ動きをする
 *
 * @param string|array|Closure $function
 * @param array $array
 * @return array
 */
function my_map(mixed $function, array $array) : array
{
    $mapped = [];

    foreach ($array as $key => $value) {
        if (is_string($function)) {
	    $mapped[$key] = $function($value);
        } elseif (is_array($function)) {
	    $mapped[$key] = $function[0]::{$function[1]}($value);
        } elseif ($function instanceof Closure) {
            $new_this = new class{};
            $mapped[$key] = $function->call($new_this, $value);
        }
    }

    return $mapped;
}

ワイ「(何やってるんかよーわからんけど)動いた!おっけーや!」
ワイ「ワイ天才!!!」

第3章 突然の callable

先生「みなさんできましたか?模範解答は、例えばこんな感じですね。」

function my_map(callable $function, array $array) : array
{
    $mapped = [];

    foreach ($array as $key => $value) {
        $mapped[$key] = $function($value);
    }

    return $mapped;
}

ワイ「ええええええええええええええ」

ワイ「なんやねん callable って!」
ワイ「いやその前に、」
ワイ「何で全部 $function($item) で動くねん!」

ワイ「パターン1はわかるで」
ワイ「元はこれなんやから、」

function double(int $item) : int
{
    return $item * 2;
}

$result = double(100); // ←これ

var_dump($result);
int(200)

ワイ「こうしても動くのは、まぁ当然や。」

function double(int $item) : int
{
    return $item * 2;
}

$function = 'double';
$result = $function(100); // ←これ

var_dump($result);
int(200)

ワイ「でも、パターン2は、こういうことやろ」

class Calculator {
    public static function double(int $item) : int
    {
        return $item * 2;
    }
}function = ['Calculator', 'double'];
$result =function(100); // ←これ

var_dump($result);

ワイ「こんなん Fatal Error に決まってr」

int(200)

ワイ「動くんかああああああああい!!」

ワイ「まさかこれも・・・」

class Calculator {
    public static function double(int $item) : int
    {
        return $item * 2;
    }
}

$result = ['Calculator', 'double'](100); // ←これ

var_dump($result);
int(200)

ワイ「動くんかああああああああい!!」

ワイ「なんでや!ただの配列やで!」
ワイ「クラス名とメソッド名が文字列で入ってるだけの配列やで!!」
ワイ「なんで関数みたいに使えんねん!」

先生(何でと言われても文法だからね・・・)

ワイ「もしかしてコレもいけるんか?」

$result = function (int $item) {
    return $item * 2;
}(100);

var_dump($result);
Parse error: syntax error, unexpected token "("

ワイ「流石にシンタックスエラーやな」
ワイ「あ、もしかしてまたカッコつけたら・・・」

$result = (function (int $item) {
    return $item * 2;
})(100);

var_dump($result);
int(200)

ワイ「いけるんかああああい!」

ワイ「つまり、」
ワイ「①関数名(文字列)」
ワイ「②クラス名とメソッド名の配列」
ワイ「③無名関数(クロージャ)」
ワイ「全部関数として呼び出せるから、callable なんやな!

先生「ワイ君、そのとおりです。」
先生「でも callable の種類は他にもたくさんあるので、後で調べておいてくださいね」

ワイ「え・・・」

第4章 array_filter 編

先生「では次は、array_filter() でやってみましょう」
ワイ(うげ、また苦手なやつや。)

先生「基本の形はこんな感じですね」

// パターン1

$array = [1, 2, 3, 4, 5];

function is_odd(int $item) : bool
{
    return ($item % 2 === 1);
}

$filtered = array_filter($array, 'is_odd');

var_dump($filtered); // [1, 3, 5]

ワイ(ふむふむ。)
ワイ(奇数かどうかを判定する is_odd() 関数があって、)
ワイ(それを $array の全部にかけてるんやな。)
ワイ(で、true が返ったやつだけが残る。)
ワイ(これならわかるで!)

先生「同じように、こんな書き方もできます。」

// パターン2

$array = [1, 2, 3, 4, 5];

class Calculator {
    public static function is_odd(int $item) : bool
    {
        return ($item % 2 === 1);
    }
}

$filtered = array_filter($array, ['Calculator', 'is_odd']);

var_dump($filtered); // [1, 3, 5]


// パターン3

$array = [1, 2, 3, 4, 5];

$filtered = array_filter($array, function (int $item) {
    return ($item % 2 === 1);
});

var_dump($filtered); // [1, 3, 5]

ワイ(また、引数に function ごにょごにょ・・・)
ワイ(でも、結局こういうことなんやな。)

// パターン3 改

$array = [1, 2, 3, 4, 5];

$function_is_odd = function (int $item) {
    return ($item % 2 === 1);
};

$filtered = array_filter($array, $function_is_odd);

var_dump($filtered); // [1, 3, 5]

ワイ(これなら、パターン1とほぼ一緒や。)
ワイ(無名関数、わかってきたで!)

先生「では、これの array_filter を、」
ワイ「ワイフィルターに変えればえぇんやな!」

<?php

// パターン1

$array = [1, 2, 3, 4, 5];

function is_odd(int $item) : bool
{
    return ($item % 2 === 1);
}

$filtered = wai_filter($array, 'is_odd');

var_dump($filtered); // [1, 3, 5]


// パターン2

$array = [1, 2, 3, 4, 5];

class Calculator {
    public static function is_odd(int $item) : bool
    {
        return ($item % 2 === 1);
    }
}

$filtered = wai_filter($array, ['Calculator', 'is_odd']);

var_dump($filtered); // [1, 3, 5]


// パターン3

$array = [1, 2, 3, 4, 5];

$filtered = wai_filter($array, function (int $item) {
    return ($item % 2 === 1);
});

var_dump($filtered); // [1, 3, 5]

/**
 * array_filter と同じ動きをする
 */
function wai_filter(array $array, callable $function) : array
{
    /** @todo */
}

ワイ「えーっと、やっぱりこれも foreach やな。」
ワイ「is_odd() にかけて、true のやつだけに絞るんやから、」

/**
 * array_filter と同じ動きをする
 *
 * @todo is_odd がハードコーディング
 */
function wai_filter(array $array, callable $function) : array
{
    $filtered = [];

    foreach ($array as $key => $value) {
        if (is_odd($value)) {
            $filtered[$key] = $value;
	}
    }

    return $filtered;
}

ワイ「今度は第2引数の $function に関数名 'is_odd' が渡ってくるんやから、」
ワイ「こうやな!」

/**
 * array_filter と同じ動きをする
 */
function wai_filter(array $array, callable $function) : array
{
    $filtered = [];

    foreach ($array as $key => $value) {
        if ($function($value)) {
            $filtered[$key] = $value;
	}
    }

    return $filtered;
}

ワイ「パターン2でもパターン3でも、callable ならこれで呼び出せるから、ヨシッ!」

ワイ「コールバック完全に理解した!」

第5章 callable いろいろ

ワイ「そういえば先生が、callable には他にも色々あるって言ってはったな。」

ワイ「まず基本形はこれやろ。」

// 1-1. 関数名を文字列で渡す

function double(int $item) : int
{
    return $item * 2;
}

$doubled = array_map('double', $array);

ワイ「やっぱり文字列で渡すって気持ち悪いな。」
ワイ「ぱっと見、ほんまに callable なんかわからへんやん。」
ワイ「あ、さっきの Closure オブジェクトに変換してやったらえぇんちゃう?」

// 1-2. 関数をクロージャ化して渡す

$closure = Closure::fromCallable('double');
$doubled = array_map($closure, $array);

ワイ「ふっふっふ、ちゃんとマニュアル読んどったで。」
ワイ「これなら静的解析でも完璧や!」

先生「PHP8.1からは、こんな書き方もできますね。」

// 1-3. 関数をファーストクラス callable 化して渡す

$callable = double(...);
$doubled = array_map($callable, $array);

ワイ「な・・・てんてんてんやと・・・」
ワイ「またよくわからん文法出てきた・・・」
ワイ「そもそもこの $callable は何やねん。」
ワイ「var_dump($callable) !」

object(Closure)#3 (1) {
  ["parameter"]=>
  array(1) {
    ["$item"]=>
    string(10) "<required>"
  }
}

ワイ「なんや、結局 Closure オブジェクトかいな!」

ワイ「次はこいつや」

// 2−1-1. クラス名と静的メソッドのメソッド名を配列で渡す
class Calculator {
    public static function double(int $item) : int
    {
        return $item * 2;
    }
}

$doubled = array_map(['Calculator', 'double'], $array);

ワイ「この配列が気持ち悪いのなんの。」
ワイ「だいたい、PHPで静的メソッドといえば、ちょんちょん(::)やないか。」
ワイ「あ、もしや、ちょんちょんでもいけるんか?」

// 2-1-2. クラスの静的メソッドをスコープ演算子表記文字列で渡す
$doubled = array_map('Calculator::double', $array);

ワイ「おお、いけるんやないか。」
ワイ「と、いうことは、」

// 2-1-3. クラスの静的メソッドをファーストクラス callable 化して渡す
$doubled = array_map(Calculator::double(...), $array);

ワイ「この、よーわからんてんてんてんも、いけるんやな・・・。」

ワイ「ちょっと待った。Calculator に namespace がある場合はどうなるんや?」

// namespaceがある場合の比較
use \App\Utilities\Calculator;

$doubled = array_map([Calculator::class, 'double'], $array);
$doubled = array_map(Calculator::class . '::double', $array);
$doubled = array_map(Calculator::double(...), $array);

ワイ「なんというか、最後のが1番スッキリして見えてきたで。」

ワイ「そういえば、static じゃないメソッドは、呼ばれへんのやろか」

class Calculator2 {
    public function double(int $item) : int
    {
        return $item * 2;
    }
}

//エラー
$doubled = array_map(['Calculator2', 'double'], $array);
Uncaught TypeError: array_map(): Argument #1 ($callback) must be a valid callback or null, non-static method Calculator2::double() cannot be called statically

ワイ「そらそうやな。Calculator2::double(100) でもエラーやからな。」
ワイ「あ、ということは、インスタンス化したらえぇんちゃうか?」

// 2−2-1. クラスのインスタンスとメソッド名を配列で渡す
$calc = new Calculator2;
$doubled = array_map([$calc, 'double'], $array);

ワイ「さすがにこれは無理やろな。」

//エラー
$calc = new Calculator2;
$doubled = array_map($calc->double(), $array);
Uncaught ArgumentCountError: Too few arguments to function Calculator2::double(), 0 passed

ワイ「そらそうや。これやと普通にメソッドが実行されてしまうからな。」
ワイ「・・・もしかして、これならいけるんか?」

// 2−2-2. クラスのメソッドをファーストクラス callable 化して渡す
$doubled = array_map($calc->double(...), $array);

ワイ「いけたわ。」

ワイ「流石に private メソッドは無理やろな。」

class Calculator3 {
    private function double(int $item) : int
    {
        return $item * 2;
    }
}

// エラー
$calc = new Calculator3;
$doubled = array_map([$calc, 'double'], $array);
Uncaught TypeError: array_map(): Argument #1 ($callback) must be a valid callback or null, cannot access private method Calculator3::double()

ワイ「まぁ、当然や。」
ワイ「ちゅーことは、内部からなら呼べるんやろうな。」

// 2−3-1. $this とプライベートメソッド名の配列を渡す
class Calculator3 {
    private function double(int $item) : int
    {
        return $item * 2;
    }

    public function do(array $array)
    {
        $doubled = array_map([$this, 'double'], $array);
    }
}

$calc = new Calculator3;
$calc->do($array);

ワイ「$this も OK っと・・・」
ワイ「ということは、これもやな。」

// 2−3-2. private メソッドをファーストクラス callable 化して渡す
$doubled = array_map($this->double(...), $array);

ワイ「後はなんや。」
ワイ「せや、最近 Laravel とかでよく使う _invoke()。」

class CalculatorDouble {
    public function __invoke(int $item) : int
    {
        return $item * 2;
    }
}

// エラー
$doubled = array_map('CalculatorDouble', $array);

ワイ「あれ、これはあかんのか。」
ワイ「そうか、関数代わりに使えるのはクラス名やなくて、インスタンスやったな。」

// 2-4-1 関数呼び出し可能(invokable)なクラスのインスタンスを渡す
$doubled = array_map(new CalculatorDouble(), $array);

ワイ「じゃあこれもいけるんか?」

// 2-4-1 関数呼び出し可能(invokable)なクラスのインスタンスを渡す
$doubled = array_map(new CalculatorDouble(...), $array);
PHP Fatal error:  Cannot create Closure for new expression

ワイ「あかん、エラーや。」
ワイ「ちゃうな。関数呼び出す引数の代わりに、てんてんてんを書くんやから、」
ワイ「こうやな」

// 2-4-2 関数呼び出し可能なクラスのインスタンスを、ファーストクラス callable 化して渡す。
$invokable = new CalculatorDouble();
$doubled = array_map($invokable(...), $array);

ワイ「流石に疲れてきたで・・・」
ワイ「あとは、このシリーズやな。」

// 3-1. 無名関数を渡す
$doubled = array_map(function (int $item) {
    return $item * 2;
}, $array);
// 3-2. 無名関数を変数に格納して渡す
$function = function (int $item) {
    return $item * 2;
};
$doubled = array_map($function, $array);

ワイ「これは流石にもうバリエーション無いやろ。」

先生「ワイ君、アロー関数って知ってます?」

// 3-3. アロー関数を渡す
$doubled = array_map(fn (int $item) => $item * 2, $array);

ワイ「も、もちろん知ってるで・・・(震え声)」
ワイ「(使ったことなかったけど、)こういう場合には確かに、function() 〜 よりスッキリしとるな!」

ワイ「callable も完全に理解したで!」

Discussion